@openacp/cli 2026.404.1 → 2026.405.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -244,6 +244,45 @@ var init_log = __esm({
244
244
  }
245
245
  });
246
246
 
247
+ // src/core/config/config-migrations.ts
248
+ function applyMigrations(raw, migrationList = migrations, ctx) {
249
+ let changed = false;
250
+ for (const migration of migrationList) {
251
+ if (migration.apply(raw, ctx)) {
252
+ changed = true;
253
+ }
254
+ }
255
+ return { changed };
256
+ }
257
+ var log2, migrations;
258
+ var init_config_migrations = __esm({
259
+ "src/core/config/config-migrations.ts"() {
260
+ "use strict";
261
+ init_log();
262
+ log2 = createChildLogger({ module: "config-migrations" });
263
+ migrations = [
264
+ {
265
+ name: "add-instance-name",
266
+ apply(raw) {
267
+ if (raw.instanceName) return false;
268
+ raw.instanceName = "Main";
269
+ log2.info("Added instanceName to config");
270
+ return true;
271
+ }
272
+ },
273
+ {
274
+ name: "delete-display-verbosity",
275
+ apply(raw) {
276
+ if (!("displayVerbosity" in raw)) return false;
277
+ delete raw.displayVerbosity;
278
+ log2.info("Removed legacy displayVerbosity key from config");
279
+ return true;
280
+ }
281
+ }
282
+ ];
283
+ }
284
+ });
285
+
247
286
  // src/core/instance/instance-context.ts
248
287
  var instance_context_exports = {};
249
288
  __export(instance_context_exports, {
@@ -308,145 +347,6 @@ var init_instance_context = __esm({
308
347
  }
309
348
  });
310
349
 
311
- // src/core/config/config-migrations.ts
312
- import * as fs3 from "fs";
313
- import * as path3 from "path";
314
- function applyMigrations(raw, migrationList = migrations, ctx) {
315
- let changed = false;
316
- for (const migration of migrationList) {
317
- if (migration.apply(raw, ctx)) {
318
- changed = true;
319
- }
320
- }
321
- return { changed };
322
- }
323
- var log2, migrations;
324
- var init_config_migrations = __esm({
325
- "src/core/config/config-migrations.ts"() {
326
- "use strict";
327
- init_log();
328
- init_instance_context();
329
- log2 = createChildLogger({ module: "config-migrations" });
330
- migrations = [
331
- {
332
- name: "add-tunnel-section",
333
- apply(raw) {
334
- if (raw.tunnel) return false;
335
- raw.tunnel = {
336
- enabled: true,
337
- port: 3100,
338
- provider: "cloudflare",
339
- options: {},
340
- storeTtlMinutes: 60,
341
- auth: { enabled: false }
342
- };
343
- log2.info("Added tunnel section to config (enabled by default with cloudflare)");
344
- return true;
345
- }
346
- },
347
- {
348
- name: "fix-agent-commands",
349
- apply(raw) {
350
- const COMMAND_MIGRATIONS = {
351
- "claude-agent-acp": ["claude", "claude-code"]
352
- };
353
- const agents = raw.agents;
354
- if (!agents || typeof agents !== "object") return false;
355
- let changed = false;
356
- for (const [agentName, agentDef] of Object.entries(agents)) {
357
- if (!agentDef || typeof agentDef !== "object" || !("command" in agentDef)) continue;
358
- const def = agentDef;
359
- if (typeof def.command !== "string") continue;
360
- for (const [correctCmd, legacyCmds] of Object.entries(COMMAND_MIGRATIONS)) {
361
- if (legacyCmds.includes(def.command)) {
362
- log2.warn(
363
- { agent: agentName, oldCommand: def.command, newCommand: correctCmd },
364
- `Auto-migrating agent command: "${def.command}" \u2192 "${correctCmd}"`
365
- );
366
- def.command = correctCmd;
367
- changed = true;
368
- }
369
- }
370
- }
371
- return changed;
372
- }
373
- },
374
- {
375
- name: "migrate-agents-to-store",
376
- apply(raw, ctx) {
377
- const agentsJsonPath = path3.join(ctx?.configDir ?? getGlobalRoot(), "agents.json");
378
- if (fs3.existsSync(agentsJsonPath)) return false;
379
- const agents = raw.agents;
380
- if (!agents || Object.keys(agents).length === 0) return false;
381
- const COMMAND_TO_REGISTRY = {
382
- "claude-agent-acp": "claude-acp",
383
- "codex": "codex-acp"
384
- };
385
- const installed = {};
386
- for (const [key, val] of Object.entries(agents)) {
387
- const cfg = val;
388
- const command = typeof cfg.command === "string" ? cfg.command : "";
389
- const registryId = COMMAND_TO_REGISTRY[command] ?? null;
390
- installed[key] = {
391
- registryId,
392
- name: key.charAt(0).toUpperCase() + key.slice(1),
393
- version: "unknown",
394
- distribution: "custom",
395
- command: cfg.command,
396
- args: cfg.args ?? [],
397
- env: cfg.env ?? {},
398
- workingDirectory: cfg.workingDirectory ?? void 0,
399
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
400
- binaryPath: null
401
- };
402
- }
403
- fs3.mkdirSync(path3.dirname(agentsJsonPath), { recursive: true });
404
- fs3.writeFileSync(agentsJsonPath, JSON.stringify({ version: 1, installed }, null, 2));
405
- raw.agents = {};
406
- return true;
407
- }
408
- },
409
- {
410
- name: "add-instance-name",
411
- apply(raw) {
412
- if (raw.instanceName) return false;
413
- raw.instanceName = "Main";
414
- log2.info("Added instanceName to config");
415
- return true;
416
- }
417
- },
418
- {
419
- name: "migrate-display-verbosity-to-output-mode",
420
- apply(raw) {
421
- const channels = raw.channels;
422
- if (!channels) return false;
423
- let changed = false;
424
- for (const [, channelCfg] of Object.entries(channels)) {
425
- if (!channelCfg || typeof channelCfg !== "object") continue;
426
- const cfg = channelCfg;
427
- if (cfg.displayVerbosity && !cfg.outputMode) {
428
- cfg.outputMode = cfg.displayVerbosity;
429
- changed = true;
430
- }
431
- }
432
- return changed;
433
- }
434
- },
435
- {
436
- name: "migrate-tunnel-provider-to-openacp",
437
- apply(raw) {
438
- const tunnel = raw.tunnel;
439
- if (!tunnel) return false;
440
- if (tunnel.provider !== "cloudflare") return false;
441
- tunnel.provider = "openacp";
442
- log2.info("Migrated tunnel provider: cloudflare \u2192 openacp (OpenACP managed tunnel)");
443
- return true;
444
- }
445
- }
446
- ];
447
- }
448
- });
449
-
450
350
  // src/core/config/config-registry.ts
451
351
  var config_registry_exports = {};
452
352
  __export(config_registry_exports, {
@@ -454,30 +354,29 @@ __export(config_registry_exports, {
454
354
  ConfigValidationError: () => ConfigValidationError,
455
355
  getConfigValue: () => getConfigValue,
456
356
  getFieldDef: () => getFieldDef,
457
- getFieldValueAsync: () => getFieldValueAsync,
458
357
  getSafeFields: () => getSafeFields,
459
358
  isHotReloadable: () => isHotReloadable,
460
359
  resolveOptions: () => resolveOptions,
461
360
  setFieldValueAsync: () => setFieldValueAsync
462
361
  });
463
- import * as fs4 from "fs";
464
- import * as path4 from "path";
465
- function getFieldDef(path35) {
466
- return CONFIG_REGISTRY.find((f) => f.path === path35);
362
+ import * as fs3 from "fs";
363
+ import * as path3 from "path";
364
+ function getFieldDef(path34) {
365
+ return CONFIG_REGISTRY.find((f) => f.path === path34);
467
366
  }
468
367
  function getSafeFields() {
469
368
  return CONFIG_REGISTRY.filter((f) => f.scope === "safe");
470
369
  }
471
- function isHotReloadable(path35) {
472
- const def = getFieldDef(path35);
370
+ function isHotReloadable(path34) {
371
+ const def = getFieldDef(path34);
473
372
  return def?.hotReload ?? false;
474
373
  }
475
374
  function resolveOptions(def, config) {
476
375
  if (!def.options) return void 0;
477
376
  return typeof def.options === "function" ? def.options(config) : def.options;
478
377
  }
479
- function getConfigValue(config, path35) {
480
- const parts = path35.split(".");
378
+ function getConfigValue(config, path34) {
379
+ const parts = path34.split(".");
481
380
  let current = config;
482
381
  for (const part of parts) {
483
382
  if (current && typeof current === "object" && part in current) {
@@ -488,13 +387,6 @@ function getConfigValue(config, path35) {
488
387
  }
489
388
  return current;
490
389
  }
491
- async function getFieldValueAsync(field, configManager, settingsManager) {
492
- if (field.plugin && settingsManager) {
493
- const settings = await settingsManager.loadSettings(field.plugin.name);
494
- return settings[field.plugin.key];
495
- }
496
- return getConfigValue(configManager.get(), field.path);
497
- }
498
390
  function validateFieldValue(field, value) {
499
391
  switch (field.type) {
500
392
  case "number":
@@ -520,17 +412,8 @@ function validateFieldValue(field, value) {
520
412
  }
521
413
  }
522
414
  }
523
- async function setFieldValueAsync(field, value, configManager, settingsManager) {
415
+ async function setFieldValueAsync(field, value, configManager) {
524
416
  validateFieldValue(field, value);
525
- if (field.plugin && settingsManager) {
526
- await settingsManager.updatePluginSettings(field.plugin.name, {
527
- [field.plugin.key]: value
528
- });
529
- if (configManager.emit) {
530
- configManager.emit("config:changed", { path: field.path, value, oldValue: void 0 });
531
- }
532
- return { needsRestart: !field.hotReload };
533
- }
534
417
  await configManager.setPath(field.path, value);
535
418
  return { needsRestart: !field.hotReload };
536
419
  }
@@ -545,38 +428,20 @@ var init_config_registry = __esm({
545
428
  displayName: "Default Agent",
546
429
  group: "agent",
547
430
  type: "select",
548
- options: (config) => {
431
+ options: () => {
549
432
  try {
550
- const agentsPath = path4.join(getGlobalRoot(), "agents.json");
551
- if (fs4.existsSync(agentsPath)) {
552
- const data = JSON.parse(fs4.readFileSync(agentsPath, "utf-8"));
433
+ const agentsPath = path3.join(getGlobalRoot(), "agents.json");
434
+ if (fs3.existsSync(agentsPath)) {
435
+ const data = JSON.parse(fs3.readFileSync(agentsPath, "utf-8"));
553
436
  return Object.keys(data.installed ?? {});
554
437
  }
555
438
  } catch {
556
439
  }
557
- return Object.keys(config.agents ?? {});
440
+ return [];
558
441
  },
559
442
  scope: "safe",
560
443
  hotReload: true
561
444
  },
562
- {
563
- path: "channels.telegram.outputMode",
564
- displayName: "Telegram Output Mode",
565
- group: "display",
566
- type: "select",
567
- options: ["low", "medium", "high"],
568
- scope: "safe",
569
- hotReload: true
570
- },
571
- {
572
- path: "channels.discord.outputMode",
573
- displayName: "Discord Output Mode",
574
- group: "display",
575
- type: "select",
576
- options: ["low", "medium", "high"],
577
- scope: "safe",
578
- hotReload: true
579
- },
580
445
  {
581
446
  path: "logging.level",
582
447
  displayName: "Log Level",
@@ -586,33 +451,6 @@ var init_config_registry = __esm({
586
451
  scope: "safe",
587
452
  hotReload: true
588
453
  },
589
- {
590
- path: "tunnel.enabled",
591
- displayName: "Tunnel",
592
- group: "tunnel",
593
- type: "toggle",
594
- scope: "safe",
595
- hotReload: false,
596
- plugin: { name: "@openacp/tunnel", key: "enabled" }
597
- },
598
- {
599
- path: "security.maxConcurrentSessions",
600
- displayName: "Max Concurrent Sessions",
601
- group: "security",
602
- type: "number",
603
- scope: "safe",
604
- hotReload: true,
605
- plugin: { name: "@openacp/security", key: "maxConcurrentSessions" }
606
- },
607
- {
608
- path: "security.sessionTimeoutMinutes",
609
- displayName: "Session Timeout (min)",
610
- group: "security",
611
- type: "number",
612
- scope: "safe",
613
- hotReload: true,
614
- plugin: { name: "@openacp/security", key: "sessionTimeoutMinutes" }
615
- },
616
454
  {
617
455
  path: "workspace.baseDir",
618
456
  displayName: "Workspace Directory",
@@ -629,25 +467,6 @@ var init_config_registry = __esm({
629
467
  scope: "safe",
630
468
  hotReload: true
631
469
  },
632
- {
633
- path: "speech.stt.provider",
634
- displayName: "Speech to Text",
635
- group: "speech",
636
- type: "select",
637
- options: ["groq"],
638
- scope: "safe",
639
- hotReload: true,
640
- plugin: { name: "@openacp/speech", key: "sttProvider" }
641
- },
642
- {
643
- path: "speech.stt.apiKey",
644
- displayName: "STT API Key",
645
- group: "speech",
646
- type: "string",
647
- scope: "sensitive",
648
- hotReload: true,
649
- plugin: { name: "@openacp/speech", key: "groqApiKey" }
650
- },
651
470
  {
652
471
  path: "agentSwitch.labelHistory",
653
472
  displayName: "Label Agent in History",
@@ -668,36 +487,24 @@ var init_config_registry = __esm({
668
487
 
669
488
  // src/core/config/config.ts
670
489
  import { z } from "zod";
671
- import * as fs5 from "fs";
672
- import * as path5 from "path";
490
+ import * as fs4 from "fs";
491
+ import * as path4 from "path";
673
492
  import * as os3 from "os";
493
+ import { randomBytes } from "crypto";
674
494
  import { EventEmitter } from "events";
675
495
  function expandHome3(p) {
676
496
  if (p.startsWith("~")) {
677
- return path5.join(os3.homedir(), p.slice(1));
497
+ return path4.join(os3.homedir(), p.slice(1));
678
498
  }
679
499
  return p;
680
500
  }
681
- var log3, BaseChannelSchema, AgentSchema, LoggingSchema, TunnelAuthSchema, TunnelSchema, UsageSchema, SpeechProviderSchema, SpeechSchema, ConfigSchema, DEFAULT_CONFIG, ConfigManager;
501
+ var log3, LoggingSchema, ConfigSchema, DEFAULT_CONFIG, ConfigManager;
682
502
  var init_config = __esm({
683
503
  "src/core/config/config.ts"() {
684
504
  "use strict";
685
505
  init_config_migrations();
686
506
  init_log();
687
507
  log3 = createChildLogger({ module: "config" });
688
- BaseChannelSchema = z.object({
689
- enabled: z.boolean().default(false),
690
- adapter: z.string().optional(),
691
- // package name for plugin adapters
692
- displayVerbosity: z.enum(["low", "medium", "high"]).optional(),
693
- outputMode: z.enum(["low", "medium", "high"]).optional()
694
- }).passthrough();
695
- AgentSchema = z.object({
696
- command: z.string(),
697
- args: z.array(z.string()).default([]),
698
- workingDirectory: z.string().optional(),
699
- env: z.record(z.string(), z.string()).default({})
700
- });
701
508
  LoggingSchema = z.object({
702
509
  level: z.enum(["silent", "debug", "info", "warn", "error", "fatal"]).default("info"),
703
510
  logDir: z.string().default("~/.openacp/logs"),
@@ -705,44 +512,8 @@ var init_config = __esm({
705
512
  maxFiles: z.number().default(7),
706
513
  sessionLogRetentionDays: z.number().default(30)
707
514
  }).default({});
708
- TunnelAuthSchema = z.object({
709
- enabled: z.boolean().default(false),
710
- token: z.string().optional()
711
- }).default({});
712
- TunnelSchema = z.object({
713
- enabled: z.boolean().default(false),
714
- port: z.number().default(3100),
715
- provider: z.enum(["openacp", "cloudflare", "ngrok", "bore", "tailscale"]).default("openacp"),
716
- options: z.record(z.string(), z.unknown()).default({}),
717
- maxUserTunnels: z.number().default(5),
718
- storeTtlMinutes: z.number().default(60),
719
- auth: TunnelAuthSchema
720
- }).default({});
721
- UsageSchema = z.object({
722
- enabled: z.boolean().default(true),
723
- monthlyBudget: z.number().optional(),
724
- warningThreshold: z.number().default(0.8),
725
- currency: z.string().default("USD"),
726
- retentionDays: z.number().default(90)
727
- }).default({});
728
- SpeechProviderSchema = z.object({
729
- apiKey: z.string().min(1).optional(),
730
- model: z.string().optional()
731
- }).passthrough();
732
- SpeechSchema = z.object({
733
- stt: z.object({
734
- provider: z.string().nullable().default(null),
735
- providers: z.record(SpeechProviderSchema).default({})
736
- }).default({}),
737
- tts: z.object({
738
- provider: z.string().nullable().default(null),
739
- providers: z.record(SpeechProviderSchema).default({})
740
- }).default({})
741
- }).optional().default({});
742
515
  ConfigSchema = z.object({
743
516
  instanceName: z.string().optional(),
744
- channels: z.object({}).catchall(BaseChannelSchema).default({}),
745
- agents: z.record(z.string(), AgentSchema).optional().default({}),
746
517
  defaultAgent: z.string(),
747
518
  workspace: z.object({
748
519
  baseDir: z.string().default("~/openacp-workspace"),
@@ -751,23 +522,12 @@ var init_config = __esm({
751
522
  envWhitelist: z.array(z.string()).default([])
752
523
  }).default({})
753
524
  }).default({}),
754
- security: z.object({
755
- allowedUserIds: z.array(z.string()).default([]),
756
- maxConcurrentSessions: z.number().default(20),
757
- sessionTimeoutMinutes: z.number().default(60)
758
- }).default({}),
759
525
  logging: LoggingSchema,
760
526
  runMode: z.enum(["foreground", "daemon"]).default("foreground"),
761
527
  autoStart: z.boolean().default(false),
762
- api: z.object({
763
- port: z.number().default(21420),
764
- host: z.string().default("127.0.0.1")
765
- }).default({}),
766
528
  sessionStore: z.object({
767
529
  ttlDays: z.number().default(30)
768
530
  }).default({}),
769
- tunnel: TunnelSchema,
770
- usage: UsageSchema,
771
531
  integrations: z.record(
772
532
  z.string(),
773
533
  z.object({
@@ -775,51 +535,15 @@ var init_config = __esm({
775
535
  installedAt: z.string().optional()
776
536
  })
777
537
  ).default({}),
778
- speech: SpeechSchema,
779
538
  outputMode: z.enum(["low", "medium", "high"]).default("medium").optional(),
780
539
  agentSwitch: z.object({
781
540
  labelHistory: z.boolean().default(true)
782
541
  }).default({})
783
542
  });
784
543
  DEFAULT_CONFIG = {
785
- channels: {
786
- telegram: {
787
- enabled: false,
788
- botToken: "YOUR_BOT_TOKEN_HERE",
789
- chatId: 0,
790
- notificationTopicId: null,
791
- assistantTopicId: null
792
- },
793
- discord: {
794
- enabled: false,
795
- botToken: "YOUR_DISCORD_BOT_TOKEN_HERE",
796
- guildId: "",
797
- forumChannelId: null,
798
- notificationChannelId: null,
799
- assistantThreadId: null
800
- }
801
- },
802
- agents: {
803
- claude: { command: "claude-agent-acp", args: [], env: {} },
804
- codex: { command: "codex", args: ["--acp"], env: {} }
805
- },
806
544
  defaultAgent: "claude",
807
545
  workspace: { baseDir: "~/openacp-workspace" },
808
- security: {
809
- allowedUserIds: [],
810
- maxConcurrentSessions: 20,
811
- sessionTimeoutMinutes: 60
812
- },
813
- sessionStore: { ttlDays: 30 },
814
- tunnel: {
815
- enabled: true,
816
- port: 3100,
817
- provider: "openacp",
818
- options: {},
819
- storeTtlMinutes: 60,
820
- auth: { enabled: false }
821
- },
822
- usage: {}
546
+ sessionStore: { ttlDays: 30 }
823
547
  };
824
548
  ConfigManager = class extends EventEmitter {
825
549
  config;
@@ -829,23 +553,23 @@ var init_config = __esm({
829
553
  this.configPath = process.env.OPENACP_CONFIG_PATH || configPath || expandHome3("~/.openacp/config.json");
830
554
  }
831
555
  async load() {
832
- const dir = path5.dirname(this.configPath);
833
- fs5.mkdirSync(dir, { recursive: true });
834
- if (!fs5.existsSync(this.configPath)) {
835
- fs5.writeFileSync(
556
+ const dir = path4.dirname(this.configPath);
557
+ fs4.mkdirSync(dir, { recursive: true });
558
+ if (!fs4.existsSync(this.configPath)) {
559
+ fs4.writeFileSync(
836
560
  this.configPath,
837
561
  JSON.stringify(DEFAULT_CONFIG, null, 2)
838
562
  );
839
563
  log3.info({ configPath: this.configPath }, "Config created");
840
564
  log3.info(
841
- "Please edit it with your channel credentials (Telegram bot token, Discord bot token, etc.), then restart."
565
+ "Run 'openacp setup' to configure channels and agents, then restart."
842
566
  );
843
567
  process.exit(1);
844
568
  }
845
- const raw = JSON.parse(fs5.readFileSync(this.configPath, "utf-8"));
846
- const { changed: configUpdated } = applyMigrations(raw, void 0, { configDir: path5.dirname(this.configPath) });
569
+ const raw = JSON.parse(fs4.readFileSync(this.configPath, "utf-8"));
570
+ const { changed: configUpdated } = applyMigrations(raw, void 0, { configDir: path4.dirname(this.configPath) });
847
571
  if (configUpdated) {
848
- fs5.writeFileSync(this.configPath, JSON.stringify(raw, null, 2));
572
+ fs4.writeFileSync(this.configPath, JSON.stringify(raw, null, 2));
849
573
  }
850
574
  this.applyEnvOverrides(raw);
851
575
  const result = ConfigSchema.safeParse(raw);
@@ -866,14 +590,16 @@ var init_config = __esm({
866
590
  }
867
591
  async save(updates, changePath) {
868
592
  const oldConfig = this.config ? structuredClone(this.config) : void 0;
869
- const raw = JSON.parse(fs5.readFileSync(this.configPath, "utf-8"));
593
+ const raw = JSON.parse(fs4.readFileSync(this.configPath, "utf-8"));
870
594
  this.deepMerge(raw, updates);
871
595
  const result = ConfigSchema.safeParse(raw);
872
596
  if (!result.success) {
873
597
  log3.error({ errors: result.error.issues }, "Config validation failed, not saving");
874
598
  return;
875
599
  }
876
- fs5.writeFileSync(this.configPath, JSON.stringify(raw, null, 2));
600
+ const tmpPath = this.configPath + `.tmp.${randomBytes(4).toString("hex")}`;
601
+ fs4.writeFileSync(tmpPath, JSON.stringify(raw, null, 2), "utf-8");
602
+ fs4.renameSync(tmpPath, this.configPath);
877
603
  this.config = result.data;
878
604
  if (changePath) {
879
605
  const { getConfigValue: getConfigValue2 } = await Promise.resolve().then(() => (init_config_registry(), config_registry_exports));
@@ -883,7 +609,7 @@ var init_config = __esm({
883
609
  }
884
610
  }
885
611
  /**
886
- * Set a single config value by dot-path (e.g. "security.maxConcurrentSessions").
612
+ * Set a single config value by dot-path (e.g. "logging.level").
887
613
  * Builds the nested update object, validates, and saves.
888
614
  * Throws if the path contains blocked keys or the value fails Zod validation.
889
615
  */
@@ -905,14 +631,14 @@ var init_config = __esm({
905
631
  resolveWorkspace(input2) {
906
632
  if (!input2) {
907
633
  const resolved2 = expandHome3(this.config.workspace.baseDir);
908
- fs5.mkdirSync(resolved2, { recursive: true });
634
+ fs4.mkdirSync(resolved2, { recursive: true });
909
635
  return resolved2;
910
636
  }
911
637
  if (input2.startsWith("/") || input2.startsWith("~")) {
912
638
  const resolved2 = expandHome3(input2);
913
639
  const base = expandHome3(this.config.workspace.baseDir);
914
- if (resolved2 === base || resolved2.startsWith(base + path5.sep)) {
915
- fs5.mkdirSync(resolved2, { recursive: true });
640
+ if (resolved2 === base || resolved2.startsWith(base + path4.sep)) {
641
+ fs4.mkdirSync(resolved2, { recursive: true });
916
642
  return resolved2;
917
643
  }
918
644
  throw new Error(
@@ -925,23 +651,23 @@ var init_config = __esm({
925
651
  `Invalid workspace name: "${input2}". Only alphanumeric characters, hyphens, and underscores are allowed.`
926
652
  );
927
653
  }
928
- const resolved = path5.join(
654
+ const resolved = path4.join(
929
655
  expandHome3(this.config.workspace.baseDir),
930
656
  name.toLowerCase()
931
657
  );
932
- fs5.mkdirSync(resolved, { recursive: true });
658
+ fs4.mkdirSync(resolved, { recursive: true });
933
659
  return resolved;
934
660
  }
935
661
  async exists() {
936
- return fs5.existsSync(this.configPath);
662
+ return fs4.existsSync(this.configPath);
937
663
  }
938
664
  getConfigPath() {
939
665
  return this.configPath;
940
666
  }
941
667
  async writeNew(config) {
942
- const dir = path5.dirname(this.configPath);
943
- fs5.mkdirSync(dir, { recursive: true });
944
- fs5.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
668
+ const dir = path4.dirname(this.configPath);
669
+ fs4.mkdirSync(dir, { recursive: true });
670
+ fs4.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
945
671
  }
946
672
  async applyEnvToPluginSettings(settingsManager) {
947
673
  const pluginOverrides = [
@@ -952,7 +678,13 @@ var init_config = __esm({
952
678
  { envVar: "OPENACP_SPEECH_STT_PROVIDER", pluginName: "@openacp/speech", key: "sttProvider" },
953
679
  { envVar: "OPENACP_SPEECH_GROQ_API_KEY", pluginName: "@openacp/speech", key: "groqApiKey" },
954
680
  { envVar: "OPENACP_TELEGRAM_BOT_TOKEN", pluginName: "@openacp/telegram", key: "botToken" },
955
- { envVar: "OPENACP_TELEGRAM_CHAT_ID", pluginName: "@openacp/telegram", key: "chatId", transform: (v) => Number(v) }
681
+ { envVar: "OPENACP_TELEGRAM_CHAT_ID", pluginName: "@openacp/telegram", key: "chatId", transform: (v) => Number(v) },
682
+ // Future adapters — no-ops if plugin settings don't exist
683
+ { envVar: "OPENACP_DISCORD_BOT_TOKEN", pluginName: "@openacp/discord-adapter", key: "botToken" },
684
+ { envVar: "OPENACP_DISCORD_GUILD_ID", pluginName: "@openacp/discord-adapter", key: "guildId" },
685
+ { envVar: "OPENACP_SLACK_BOT_TOKEN", pluginName: "@openacp/slack-adapter", key: "botToken" },
686
+ { envVar: "OPENACP_SLACK_APP_TOKEN", pluginName: "@openacp/slack-adapter", key: "appToken" },
687
+ { envVar: "OPENACP_SLACK_SIGNING_SECRET", pluginName: "@openacp/slack-adapter", key: "signingSecret" }
956
688
  ];
957
689
  for (const { envVar, pluginName, key, transform } of pluginOverrides) {
958
690
  const value = process.env[envVar];
@@ -965,16 +697,8 @@ var init_config = __esm({
965
697
  }
966
698
  applyEnvOverrides(raw) {
967
699
  const overrides = [
968
- ["OPENACP_TELEGRAM_BOT_TOKEN", ["channels", "telegram", "botToken"]],
969
- ["OPENACP_TELEGRAM_CHAT_ID", ["channels", "telegram", "chatId"]],
970
- ["OPENACP_DISCORD_BOT_TOKEN", ["channels", "discord", "botToken"]],
971
- ["OPENACP_DISCORD_GUILD_ID", ["channels", "discord", "guildId"]],
972
- ["OPENACP_SLACK_BOT_TOKEN", ["channels", "slack", "botToken"]],
973
- ["OPENACP_SLACK_APP_TOKEN", ["channels", "slack", "appToken"]],
974
- ["OPENACP_SLACK_SIGNING_SECRET", ["channels", "slack", "signingSecret"]],
975
700
  ["OPENACP_DEFAULT_AGENT", ["defaultAgent"]],
976
- ["OPENACP_RUN_MODE", ["runMode"]],
977
- ["OPENACP_API_PORT", ["api", "port"]]
701
+ ["OPENACP_RUN_MODE", ["runMode"]]
978
702
  ];
979
703
  for (const [envVar, configPath] of overrides) {
980
704
  const value = process.env[envVar];
@@ -985,7 +709,7 @@ var init_config = __esm({
985
709
  target = target[configPath[i]];
986
710
  }
987
711
  const key = configPath[configPath.length - 1];
988
- target[key] = key === "chatId" || key === "port" ? Number(value) : value;
712
+ target[key] = value;
989
713
  }
990
714
  }
991
715
  if (process.env.OPENACP_LOG_LEVEL) {
@@ -1000,39 +724,11 @@ var init_config = __esm({
1000
724
  raw.logging = raw.logging || {};
1001
725
  raw.logging.level = "debug";
1002
726
  }
1003
- if (process.env.OPENACP_TUNNEL_ENABLED) {
1004
- raw.tunnel = raw.tunnel || {};
1005
- raw.tunnel.enabled = process.env.OPENACP_TUNNEL_ENABLED === "true";
1006
- }
1007
- if (process.env.OPENACP_TUNNEL_PORT) {
1008
- raw.tunnel = raw.tunnel || {};
1009
- raw.tunnel.port = Number(
1010
- process.env.OPENACP_TUNNEL_PORT
1011
- );
1012
- }
1013
- if (process.env.OPENACP_TUNNEL_PROVIDER) {
1014
- raw.tunnel = raw.tunnel || {};
1015
- raw.tunnel.provider = process.env.OPENACP_TUNNEL_PROVIDER;
1016
- }
1017
- if (process.env.OPENACP_SPEECH_STT_PROVIDER) {
1018
- raw.speech = raw.speech || {};
1019
- const speech = raw.speech;
1020
- speech.stt = speech.stt || {};
1021
- speech.stt.provider = process.env.OPENACP_SPEECH_STT_PROVIDER;
1022
- }
1023
- if (process.env.OPENACP_SPEECH_GROQ_API_KEY) {
1024
- raw.speech = raw.speech || {};
1025
- const speech = raw.speech;
1026
- speech.stt = speech.stt || {};
1027
- const stt = speech.stt;
1028
- stt.providers = stt.providers || {};
1029
- const providers = stt.providers;
1030
- providers.groq = providers.groq || {};
1031
- providers.groq.apiKey = process.env.OPENACP_SPEECH_GROQ_API_KEY;
1032
- }
1033
727
  }
1034
728
  deepMerge(target, source) {
729
+ const DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1035
730
  for (const key of Object.keys(source)) {
731
+ if (DANGEROUS_KEYS.has(key)) continue;
1036
732
  const val = source[key];
1037
733
  if (val && typeof val === "object" && !Array.isArray(val)) {
1038
734
  if (!target[key]) target[key] = {};
@@ -1054,9 +750,9 @@ var read_text_file_exports = {};
1054
750
  __export(read_text_file_exports, {
1055
751
  readTextFileWithRange: () => readTextFileWithRange
1056
752
  });
1057
- import fs7 from "fs";
753
+ import fs6 from "fs";
1058
754
  async function readTextFileWithRange(filePath, options) {
1059
- const content = await fs7.promises.readFile(filePath, "utf-8");
755
+ const content = await fs6.promises.readFile(filePath, "utf-8");
1060
756
  if (!options?.line && !options?.limit) return content;
1061
757
  const lines = content.split("\n");
1062
758
  const start = Math.max(0, (options.line ?? 1) - 1);
@@ -1096,8 +792,8 @@ __export(agent_dependencies_exports, {
1096
792
  listAgentsWithIntegration: () => listAgentsWithIntegration
1097
793
  });
1098
794
  import { execFileSync as execFileSync2 } from "child_process";
1099
- import * as fs12 from "fs";
1100
- import * as path11 from "path";
795
+ import * as fs11 from "fs";
796
+ import * as path10 from "path";
1101
797
  function getAgentSetup(registryId) {
1102
798
  return AGENT_SETUP[registryId];
1103
799
  }
@@ -1121,9 +817,9 @@ function commandExists(cmd) {
1121
817
  }
1122
818
  let dir = process.cwd();
1123
819
  while (true) {
1124
- const binPath = path11.join(dir, "node_modules", ".bin", cmd);
1125
- if (fs12.existsSync(binPath)) return true;
1126
- const parent = path11.dirname(dir);
820
+ const binPath = path10.join(dir, "node_modules", ".bin", cmd);
821
+ if (fs11.existsSync(binPath)) return true;
822
+ const parent = path10.dirname(dir);
1127
823
  if (parent === dir) break;
1128
824
  dir = parent;
1129
825
  }
@@ -1291,6 +987,7 @@ var init_agent_dependencies = __esm({
1291
987
  supportsResume: true,
1292
988
  resumeCommand: (sid) => `claude --resume ${sid}`,
1293
989
  integration: {
990
+ strategy: "hooks",
1294
991
  hookEvent: "UserPromptSubmit",
1295
992
  settingsPath: "~/.claude/settings.json",
1296
993
  settingsFormat: "settings_json",
@@ -1308,6 +1005,7 @@ var init_agent_dependencies = __esm({
1308
1005
  supportsResume: true,
1309
1006
  resumeCommand: (sid) => `cursor --resume ${sid}`,
1310
1007
  integration: {
1008
+ strategy: "hooks",
1311
1009
  hookEvent: "beforeSubmitPrompt",
1312
1010
  settingsPath: "~/.cursor/hooks.json",
1313
1011
  settingsFormat: "hooks_json",
@@ -1323,6 +1021,7 @@ var init_agent_dependencies = __esm({
1323
1021
  supportsResume: true,
1324
1022
  resumeCommand: (sid) => `gemini --resume ${sid}`,
1325
1023
  integration: {
1024
+ strategy: "hooks",
1326
1025
  hookEvent: "BeforeAgent",
1327
1026
  settingsPath: "~/.gemini/settings.json",
1328
1027
  settingsFormat: "settings_json",
@@ -1335,6 +1034,7 @@ var init_agent_dependencies = __esm({
1335
1034
  supportsResume: true,
1336
1035
  resumeCommand: () => `cline --continue`,
1337
1036
  integration: {
1037
+ strategy: "hooks",
1338
1038
  hookEvent: "TaskStart",
1339
1039
  settingsPath: "~/.cline/settings.json",
1340
1040
  settingsFormat: "settings_json",
@@ -1354,6 +1054,19 @@ var init_agent_dependencies = __esm({
1354
1054
  amp: {
1355
1055
  supportsResume: true,
1356
1056
  resumeCommand: (sid) => `amp threads continue ${sid}`
1057
+ },
1058
+ opencode: {
1059
+ supportsResume: true,
1060
+ resumeCommand: (sid) => `opencode --session ${sid}`,
1061
+ integration: {
1062
+ strategy: "plugin",
1063
+ pluginProvider: "opencode",
1064
+ commandsPath: "~/.config/opencode/commands/",
1065
+ pluginsPath: "~/.config/opencode/plugins/",
1066
+ handoffCommandName: "openacp:handoff",
1067
+ handoffCommandFile: "openacp-handoff.md",
1068
+ pluginFileName: "openacp-handoff.js"
1069
+ }
1357
1070
  }
1358
1071
  };
1359
1072
  REGISTRY_AGENT_ALIASES = {
@@ -1379,11 +1092,11 @@ var init_agent_registry = __esm({
1379
1092
  });
1380
1093
 
1381
1094
  // src/core/agents/agent-installer.ts
1382
- import * as fs14 from "fs";
1383
- import * as path13 from "path";
1095
+ import * as fs13 from "fs";
1096
+ import * as path12 from "path";
1384
1097
  import * as os5 from "os";
1385
1098
  import crypto from "crypto";
1386
- function validateTarContents(entries, destDir) {
1099
+ function validateArchiveContents(entries, destDir) {
1387
1100
  for (const entry of entries) {
1388
1101
  const segments = entry.split("/");
1389
1102
  if (segments.includes("..")) {
@@ -1395,9 +1108,9 @@ function validateTarContents(entries, destDir) {
1395
1108
  }
1396
1109
  }
1397
1110
  function validateUninstallPath(binaryPath, agentsDir) {
1398
- const realPath = path13.resolve(binaryPath);
1399
- const realAgentsDir = path13.resolve(agentsDir);
1400
- if (!realPath.startsWith(realAgentsDir + path13.sep) && realPath !== realAgentsDir) {
1111
+ const realPath = path12.resolve(binaryPath);
1112
+ const realAgentsDir = path12.resolve(agentsDir);
1113
+ if (!realPath.startsWith(realAgentsDir + path12.sep) && realPath !== realAgentsDir) {
1401
1114
  throw new Error(`Refusing to delete path outside agents directory: ${realPath}`);
1402
1115
  }
1403
1116
  }
@@ -1451,7 +1164,7 @@ function buildInstalledAgent(registryId, name, version, dist, binaryPath) {
1451
1164
  binaryPath: null
1452
1165
  };
1453
1166
  }
1454
- const absCmd = path13.resolve(binaryPath, dist.cmd);
1167
+ const absCmd = path12.resolve(binaryPath, dist.cmd);
1455
1168
  return {
1456
1169
  registryId,
1457
1170
  name,
@@ -1524,8 +1237,8 @@ Install it with: pip install uv`;
1524
1237
  return { ok: true, agentKey, setupSteps: setup?.setupSteps };
1525
1238
  }
1526
1239
  async function downloadAndExtract(agentId, archiveUrl, progress, agentsDir) {
1527
- const destDir = path13.join(agentsDir ?? DEFAULT_AGENTS_DIR, agentId);
1528
- fs14.mkdirSync(destDir, { recursive: true });
1240
+ const destDir = path12.join(agentsDir ?? DEFAULT_AGENTS_DIR, agentId);
1241
+ fs13.mkdirSync(destDir, { recursive: true });
1529
1242
  await progress?.onStep("Downloading...");
1530
1243
  log11.info({ agentId, url: archiveUrl }, "Downloading agent binary");
1531
1244
  const response = await fetch(archiveUrl);
@@ -1566,68 +1279,72 @@ async function readResponseWithProgress(response, contentLength, progress) {
1566
1279
  return Buffer.concat(chunks);
1567
1280
  }
1568
1281
  function validateExtractedPaths(destDir) {
1569
- const realDest = fs14.realpathSync(destDir);
1570
- const entries = fs14.readdirSync(destDir, { recursive: true, withFileTypes: true });
1282
+ const realDest = fs13.realpathSync(destDir);
1283
+ const entries = fs13.readdirSync(destDir, { recursive: true, withFileTypes: true });
1571
1284
  for (const entry of entries) {
1572
1285
  const dirent = entry;
1573
1286
  const parentPath = dirent.parentPath ?? dirent.path ?? destDir;
1574
- const fullPath = path13.join(parentPath, entry.name);
1287
+ const fullPath = path12.join(parentPath, entry.name);
1575
1288
  let realPath;
1576
1289
  try {
1577
- realPath = fs14.realpathSync(fullPath);
1290
+ realPath = fs13.realpathSync(fullPath);
1578
1291
  } catch {
1579
- const linkTarget = fs14.readlinkSync(fullPath);
1580
- realPath = path13.resolve(path13.dirname(fullPath), linkTarget);
1292
+ const linkTarget = fs13.readlinkSync(fullPath);
1293
+ realPath = path12.resolve(path12.dirname(fullPath), linkTarget);
1581
1294
  }
1582
- if (!realPath.startsWith(realDest + path13.sep) && realPath !== realDest) {
1583
- fs14.rmSync(destDir, { recursive: true, force: true });
1295
+ if (!realPath.startsWith(realDest + path12.sep) && realPath !== realDest) {
1296
+ fs13.rmSync(destDir, { recursive: true, force: true });
1584
1297
  throw new Error(`Archive contains unsafe path: ${entry.name}`);
1585
1298
  }
1586
1299
  }
1587
1300
  }
1588
1301
  async function extractTarGz(buffer, destDir) {
1589
- const { execFileSync: execFileSync7 } = await import("child_process");
1590
- const tmpFile = path13.join(destDir, "_archive.tar.gz");
1591
- fs14.writeFileSync(tmpFile, buffer);
1302
+ const { execFileSync: execFileSync8 } = await import("child_process");
1303
+ const tmpFile = path12.join(destDir, "_archive.tar.gz");
1304
+ fs13.writeFileSync(tmpFile, buffer);
1592
1305
  try {
1593
- const listing = execFileSync7("tar", ["tf", tmpFile], { stdio: "pipe" }).toString().trim().split("\n").filter(Boolean);
1594
- validateTarContents(listing, destDir);
1595
- execFileSync7("tar", ["xzf", tmpFile, "-C", destDir], { stdio: "pipe" });
1306
+ const listing = execFileSync8("tar", ["tf", tmpFile], { stdio: "pipe" }).toString().trim().split("\n").filter(Boolean);
1307
+ validateArchiveContents(listing, destDir);
1308
+ execFileSync8("tar", ["xzf", tmpFile, "-C", destDir], { stdio: "pipe" });
1596
1309
  } finally {
1597
- fs14.unlinkSync(tmpFile);
1310
+ fs13.unlinkSync(tmpFile);
1598
1311
  }
1599
1312
  validateExtractedPaths(destDir);
1600
1313
  }
1601
1314
  async function extractZip(buffer, destDir) {
1602
- const { execFileSync: execFileSync7 } = await import("child_process");
1603
- const tmpFile = path13.join(destDir, "_archive.zip");
1604
- fs14.writeFileSync(tmpFile, buffer);
1315
+ const { execFileSync: execFileSync8 } = await import("child_process");
1316
+ const tmpFile = path12.join(destDir, "_archive.zip");
1317
+ fs13.writeFileSync(tmpFile, buffer);
1605
1318
  try {
1606
- execFileSync7("unzip", ["-o", tmpFile, "-d", destDir], { stdio: "pipe" });
1319
+ const listing = execFileSync8("unzip", ["-l", tmpFile], { stdio: "pipe" }).toString().trim().split("\n").filter(Boolean);
1320
+ const entries = listing.slice(3, -2).map((line) => line.trim().split(/\s+/).slice(3).join(" ")).filter(Boolean);
1321
+ validateArchiveContents(entries, destDir);
1322
+ execFileSync8("unzip", ["-o", tmpFile, "-d", destDir], { stdio: "pipe" });
1607
1323
  } finally {
1608
- fs14.unlinkSync(tmpFile);
1324
+ fs13.unlinkSync(tmpFile);
1609
1325
  }
1610
1326
  validateExtractedPaths(destDir);
1611
1327
  }
1612
1328
  async function uninstallAgent(agentKey, store, agentsDir) {
1613
1329
  const agent = store.getAgent(agentKey);
1614
1330
  if (!agent) return;
1615
- if (agent.binaryPath && fs14.existsSync(agent.binaryPath)) {
1331
+ if (agent.binaryPath && fs13.existsSync(agent.binaryPath)) {
1616
1332
  validateUninstallPath(agent.binaryPath, agentsDir ?? DEFAULT_AGENTS_DIR);
1617
- fs14.rmSync(agent.binaryPath, { recursive: true, force: true });
1333
+ fs13.rmSync(agent.binaryPath, { recursive: true, force: true });
1618
1334
  log11.info({ agentKey, binaryPath: agent.binaryPath }, "Deleted agent binary");
1619
1335
  }
1620
1336
  store.removeAgent(agentKey);
1621
1337
  }
1622
- var log11, DEFAULT_AGENTS_DIR, MAX_DOWNLOAD_SIZE, ARCH_MAP, PLATFORM_MAP;
1338
+ var log11, DEFAULT_AGENTS_DIR, MAX_DOWNLOAD_SIZE, validateTarContents, ARCH_MAP, PLATFORM_MAP;
1623
1339
  var init_agent_installer = __esm({
1624
1340
  "src/core/agents/agent-installer.ts"() {
1625
1341
  "use strict";
1626
1342
  init_log();
1627
1343
  init_agent_dependencies();
1628
1344
  log11 = createChildLogger({ module: "agent-installer" });
1629
- DEFAULT_AGENTS_DIR = path13.join(os5.homedir(), ".openacp", "agents");
1345
+ DEFAULT_AGENTS_DIR = path12.join(os5.homedir(), ".openacp", "agents");
1630
1346
  MAX_DOWNLOAD_SIZE = 500 * 1024 * 1024;
1347
+ validateTarContents = validateArchiveContents;
1631
1348
  ARCH_MAP = {
1632
1349
  arm64: "aarch64",
1633
1350
  x64: "x86_64"
@@ -1641,7 +1358,7 @@ var init_agent_installer = __esm({
1641
1358
  });
1642
1359
 
1643
1360
  // src/core/doctor/checks/config.ts
1644
- import * as fs17 from "fs";
1361
+ import * as fs16 from "fs";
1645
1362
  var configCheck;
1646
1363
  var init_config2 = __esm({
1647
1364
  "src/core/doctor/checks/config.ts"() {
@@ -1653,14 +1370,14 @@ var init_config2 = __esm({
1653
1370
  order: 1,
1654
1371
  async run(ctx) {
1655
1372
  const results = [];
1656
- if (!fs17.existsSync(ctx.configPath)) {
1373
+ if (!fs16.existsSync(ctx.configPath)) {
1657
1374
  results.push({ status: "fail", message: "Config file not found" });
1658
1375
  return results;
1659
1376
  }
1660
1377
  results.push({ status: "pass", message: "Config file exists" });
1661
1378
  let raw;
1662
1379
  try {
1663
- raw = JSON.parse(fs17.readFileSync(ctx.configPath, "utf-8"));
1380
+ raw = JSON.parse(fs16.readFileSync(ctx.configPath, "utf-8"));
1664
1381
  } catch (err) {
1665
1382
  results.push({
1666
1383
  status: "fail",
@@ -1679,7 +1396,7 @@ var init_config2 = __esm({
1679
1396
  fixRisk: "safe",
1680
1397
  fix: async () => {
1681
1398
  applyMigrations(raw);
1682
- fs17.writeFileSync(ctx.configPath, JSON.stringify(raw, null, 2));
1399
+ fs16.writeFileSync(ctx.configPath, JSON.stringify(raw, null, 2));
1683
1400
  return { success: true, message: "applied migrations" };
1684
1401
  }
1685
1402
  });
@@ -1703,8 +1420,8 @@ var init_config2 = __esm({
1703
1420
 
1704
1421
  // src/core/doctor/checks/agents.ts
1705
1422
  import { execFileSync as execFileSync3 } from "child_process";
1706
- import * as fs18 from "fs";
1707
- import * as path18 from "path";
1423
+ import * as fs17 from "fs";
1424
+ import * as path17 from "path";
1708
1425
  function commandExists2(cmd) {
1709
1426
  try {
1710
1427
  execFileSync3("which", [cmd], { stdio: "pipe" });
@@ -1713,9 +1430,9 @@ function commandExists2(cmd) {
1713
1430
  }
1714
1431
  let dir = process.cwd();
1715
1432
  while (true) {
1716
- const binPath = path18.join(dir, "node_modules", ".bin", cmd);
1717
- if (fs18.existsSync(binPath)) return true;
1718
- const parent = path18.dirname(dir);
1433
+ const binPath = path17.join(dir, "node_modules", ".bin", cmd);
1434
+ if (fs17.existsSync(binPath)) return true;
1435
+ const parent = path17.dirname(dir);
1719
1436
  if (parent === dir) break;
1720
1437
  dir = parent;
1721
1438
  }
@@ -1734,8 +1451,16 @@ var init_agents = __esm({
1734
1451
  results.push({ status: "fail", message: "Cannot check agents \u2014 config not loaded" });
1735
1452
  return results;
1736
1453
  }
1737
- const agents = ctx.config.agents;
1738
1454
  const defaultAgent = ctx.config.defaultAgent;
1455
+ let agents = {};
1456
+ try {
1457
+ const agentsPath = path17.join(ctx.dataDir, "agents.json");
1458
+ if (fs17.existsSync(agentsPath)) {
1459
+ const data = JSON.parse(fs17.readFileSync(agentsPath, "utf-8"));
1460
+ agents = data.installed ?? {};
1461
+ }
1462
+ } catch {
1463
+ }
1739
1464
  if (!agents[defaultAgent]) {
1740
1465
  results.push({
1741
1466
  status: "fail",
@@ -1744,15 +1469,17 @@ var init_agents = __esm({
1744
1469
  }
1745
1470
  for (const [name, agent] of Object.entries(agents)) {
1746
1471
  const isDefault = name === defaultAgent;
1747
- if (commandExists2(agent.command)) {
1472
+ const agentEntry = agent;
1473
+ const agentCommand = agentEntry.command ?? name;
1474
+ if (commandExists2(agentCommand)) {
1748
1475
  results.push({
1749
1476
  status: "pass",
1750
- message: `${agent.command} found${isDefault ? " (default)" : ""}`
1477
+ message: `${agentCommand} found${isDefault ? " (default)" : ""}`
1751
1478
  });
1752
1479
  } else {
1753
1480
  results.push({
1754
1481
  status: isDefault ? "fail" : "warn",
1755
- message: `${agent.command} not found in PATH${isDefault ? " (default agent!)" : ""}`
1482
+ message: `${agentCommand} not found in PATH${isDefault ? " (default agent!)" : ""}`
1756
1483
  });
1757
1484
  }
1758
1485
  }
@@ -1767,8 +1494,8 @@ var settings_manager_exports = {};
1767
1494
  __export(settings_manager_exports, {
1768
1495
  SettingsManager: () => SettingsManager
1769
1496
  });
1770
- import fs19 from "fs";
1771
- import path19 from "path";
1497
+ import fs18 from "fs";
1498
+ import path18 from "path";
1772
1499
  var SettingsManager, SettingsAPIImpl;
1773
1500
  var init_settings_manager = __esm({
1774
1501
  "src/core/plugin/settings-manager.ts"() {
@@ -1787,7 +1514,7 @@ var init_settings_manager = __esm({
1787
1514
  async loadSettings(pluginName) {
1788
1515
  const settingsPath = this.getSettingsPath(pluginName);
1789
1516
  try {
1790
- const content = fs19.readFileSync(settingsPath, "utf-8");
1517
+ const content = fs18.readFileSync(settingsPath, "utf-8");
1791
1518
  return JSON.parse(content);
1792
1519
  } catch {
1793
1520
  return {};
@@ -1805,7 +1532,7 @@ var init_settings_manager = __esm({
1805
1532
  };
1806
1533
  }
1807
1534
  getSettingsPath(pluginName) {
1808
- return path19.join(this.basePath, pluginName, "settings.json");
1535
+ return path18.join(this.basePath, pluginName, "settings.json");
1809
1536
  }
1810
1537
  async getPluginSettings(pluginName) {
1811
1538
  return this.loadSettings(pluginName);
@@ -1824,7 +1551,7 @@ var init_settings_manager = __esm({
1824
1551
  readFile() {
1825
1552
  if (this.cache !== null) return this.cache;
1826
1553
  try {
1827
- const content = fs19.readFileSync(this.settingsPath, "utf-8");
1554
+ const content = fs18.readFileSync(this.settingsPath, "utf-8");
1828
1555
  this.cache = JSON.parse(content);
1829
1556
  return this.cache;
1830
1557
  } catch {
@@ -1833,9 +1560,9 @@ var init_settings_manager = __esm({
1833
1560
  }
1834
1561
  }
1835
1562
  writeFile(data) {
1836
- const dir = path19.dirname(this.settingsPath);
1837
- fs19.mkdirSync(dir, { recursive: true });
1838
- fs19.writeFileSync(this.settingsPath, JSON.stringify(data, null, 2));
1563
+ const dir = path18.dirname(this.settingsPath);
1564
+ fs18.mkdirSync(dir, { recursive: true });
1565
+ fs18.writeFileSync(this.settingsPath, JSON.stringify(data, null, 2));
1839
1566
  this.cache = data;
1840
1567
  }
1841
1568
  async get(key) {
@@ -1870,7 +1597,7 @@ var init_settings_manager = __esm({
1870
1597
  });
1871
1598
 
1872
1599
  // src/core/doctor/checks/telegram.ts
1873
- import * as path20 from "path";
1600
+ import * as path19 from "path";
1874
1601
  var BOT_TOKEN_REGEX, telegramCheck;
1875
1602
  var init_telegram = __esm({
1876
1603
  "src/core/doctor/checks/telegram.ts"() {
@@ -1886,11 +1613,10 @@ var init_telegram = __esm({
1886
1613
  return results;
1887
1614
  }
1888
1615
  const { SettingsManager: SettingsManager2 } = await Promise.resolve().then(() => (init_settings_manager(), settings_manager_exports));
1889
- const sm = new SettingsManager2(path20.join(ctx.pluginsDir, "data"));
1616
+ const sm = new SettingsManager2(path19.join(ctx.pluginsDir, "data"));
1890
1617
  const ps = await sm.loadSettings("@openacp/telegram");
1891
- const legacyCh = ctx.config.channels.telegram;
1892
- const botToken = ps.botToken ?? legacyCh?.botToken;
1893
- const chatId = ps.chatId ?? legacyCh?.chatId;
1618
+ const botToken = ps.botToken;
1619
+ const chatId = ps.chatId;
1894
1620
  if (!botToken && !chatId) {
1895
1621
  results.push({ status: "pass", message: "Telegram not configured (skipped)" });
1896
1622
  return results;
@@ -1970,7 +1696,7 @@ var init_telegram = __esm({
1970
1696
  });
1971
1697
 
1972
1698
  // src/core/doctor/checks/storage.ts
1973
- import * as fs20 from "fs";
1699
+ import * as fs19 from "fs";
1974
1700
  var storageCheck;
1975
1701
  var init_storage = __esm({
1976
1702
  "src/core/doctor/checks/storage.ts"() {
@@ -1980,28 +1706,28 @@ var init_storage = __esm({
1980
1706
  order: 4,
1981
1707
  async run(ctx) {
1982
1708
  const results = [];
1983
- if (!fs20.existsSync(ctx.dataDir)) {
1709
+ if (!fs19.existsSync(ctx.dataDir)) {
1984
1710
  results.push({
1985
1711
  status: "fail",
1986
1712
  message: "Data directory ~/.openacp does not exist",
1987
1713
  fixable: true,
1988
1714
  fixRisk: "safe",
1989
1715
  fix: async () => {
1990
- fs20.mkdirSync(ctx.dataDir, { recursive: true });
1716
+ fs19.mkdirSync(ctx.dataDir, { recursive: true });
1991
1717
  return { success: true, message: "created directory" };
1992
1718
  }
1993
1719
  });
1994
1720
  } else {
1995
1721
  try {
1996
- fs20.accessSync(ctx.dataDir, fs20.constants.W_OK);
1722
+ fs19.accessSync(ctx.dataDir, fs19.constants.W_OK);
1997
1723
  results.push({ status: "pass", message: "Data directory exists and writable" });
1998
1724
  } catch {
1999
1725
  results.push({ status: "fail", message: "Data directory not writable" });
2000
1726
  }
2001
1727
  }
2002
- if (fs20.existsSync(ctx.sessionsPath)) {
1728
+ if (fs19.existsSync(ctx.sessionsPath)) {
2003
1729
  try {
2004
- const content = fs20.readFileSync(ctx.sessionsPath, "utf-8");
1730
+ const content = fs19.readFileSync(ctx.sessionsPath, "utf-8");
2005
1731
  const data = JSON.parse(content);
2006
1732
  if (typeof data === "object" && data !== null && "sessions" in data) {
2007
1733
  results.push({ status: "pass", message: "Sessions file valid" });
@@ -2012,7 +1738,7 @@ var init_storage = __esm({
2012
1738
  fixable: true,
2013
1739
  fixRisk: "risky",
2014
1740
  fix: async () => {
2015
- fs20.writeFileSync(ctx.sessionsPath, JSON.stringify({ version: 1, sessions: {} }, null, 2));
1741
+ fs19.writeFileSync(ctx.sessionsPath, JSON.stringify({ version: 1, sessions: {} }, null, 2));
2016
1742
  return { success: true, message: "reset sessions file" };
2017
1743
  }
2018
1744
  });
@@ -2024,7 +1750,7 @@ var init_storage = __esm({
2024
1750
  fixable: true,
2025
1751
  fixRisk: "risky",
2026
1752
  fix: async () => {
2027
- fs20.writeFileSync(ctx.sessionsPath, JSON.stringify({ version: 1, sessions: {} }, null, 2));
1753
+ fs19.writeFileSync(ctx.sessionsPath, JSON.stringify({ version: 1, sessions: {} }, null, 2));
2028
1754
  return { success: true, message: "reset sessions file" };
2029
1755
  }
2030
1756
  });
@@ -2032,20 +1758,20 @@ var init_storage = __esm({
2032
1758
  } else {
2033
1759
  results.push({ status: "pass", message: "Sessions file not present yet (created on first session)" });
2034
1760
  }
2035
- if (!fs20.existsSync(ctx.logsDir)) {
1761
+ if (!fs19.existsSync(ctx.logsDir)) {
2036
1762
  results.push({
2037
1763
  status: "warn",
2038
1764
  message: "Log directory does not exist",
2039
1765
  fixable: true,
2040
1766
  fixRisk: "safe",
2041
1767
  fix: async () => {
2042
- fs20.mkdirSync(ctx.logsDir, { recursive: true });
1768
+ fs19.mkdirSync(ctx.logsDir, { recursive: true });
2043
1769
  return { success: true, message: "created log directory" };
2044
1770
  }
2045
1771
  });
2046
1772
  } else {
2047
1773
  try {
2048
- fs20.accessSync(ctx.logsDir, fs20.constants.W_OK);
1774
+ fs19.accessSync(ctx.logsDir, fs19.constants.W_OK);
2049
1775
  results.push({ status: "pass", message: "Log directory exists and writable" });
2050
1776
  } catch {
2051
1777
  results.push({ status: "fail", message: "Log directory not writable" });
@@ -2058,7 +1784,7 @@ var init_storage = __esm({
2058
1784
  });
2059
1785
 
2060
1786
  // src/core/doctor/checks/workspace.ts
2061
- import * as fs21 from "fs";
1787
+ import * as fs20 from "fs";
2062
1788
  var workspaceCheck;
2063
1789
  var init_workspace = __esm({
2064
1790
  "src/core/doctor/checks/workspace.ts"() {
@@ -2074,20 +1800,20 @@ var init_workspace = __esm({
2074
1800
  return results;
2075
1801
  }
2076
1802
  const baseDir = expandHome3(ctx.config.workspace.baseDir);
2077
- if (!fs21.existsSync(baseDir)) {
1803
+ if (!fs20.existsSync(baseDir)) {
2078
1804
  results.push({
2079
1805
  status: "warn",
2080
1806
  message: `Workspace directory does not exist: ${baseDir}`,
2081
1807
  fixable: true,
2082
1808
  fixRisk: "safe",
2083
1809
  fix: async () => {
2084
- fs21.mkdirSync(baseDir, { recursive: true });
1810
+ fs20.mkdirSync(baseDir, { recursive: true });
2085
1811
  return { success: true, message: "created directory" };
2086
1812
  }
2087
1813
  });
2088
1814
  } else {
2089
1815
  try {
2090
- fs21.accessSync(baseDir, fs21.constants.W_OK);
1816
+ fs20.accessSync(baseDir, fs20.constants.W_OK);
2091
1817
  results.push({ status: "pass", message: `Workspace directory exists: ${baseDir}` });
2092
1818
  } catch {
2093
1819
  results.push({ status: "fail", message: `Workspace directory not writable: ${baseDir}` });
@@ -2100,8 +1826,8 @@ var init_workspace = __esm({
2100
1826
  });
2101
1827
 
2102
1828
  // src/core/doctor/checks/plugins.ts
2103
- import * as fs22 from "fs";
2104
- import * as path21 from "path";
1829
+ import * as fs21 from "fs";
1830
+ import * as path20 from "path";
2105
1831
  var pluginsCheck;
2106
1832
  var init_plugins = __esm({
2107
1833
  "src/core/doctor/checks/plugins.ts"() {
@@ -2111,16 +1837,16 @@ var init_plugins = __esm({
2111
1837
  order: 6,
2112
1838
  async run(ctx) {
2113
1839
  const results = [];
2114
- if (!fs22.existsSync(ctx.pluginsDir)) {
1840
+ if (!fs21.existsSync(ctx.pluginsDir)) {
2115
1841
  results.push({
2116
1842
  status: "warn",
2117
1843
  message: "Plugins directory does not exist",
2118
1844
  fixable: true,
2119
1845
  fixRisk: "safe",
2120
1846
  fix: async () => {
2121
- fs22.mkdirSync(ctx.pluginsDir, { recursive: true });
2122
- fs22.writeFileSync(
2123
- path21.join(ctx.pluginsDir, "package.json"),
1847
+ fs21.mkdirSync(ctx.pluginsDir, { recursive: true });
1848
+ fs21.writeFileSync(
1849
+ path20.join(ctx.pluginsDir, "package.json"),
2124
1850
  JSON.stringify({ name: "openacp-plugins", private: true, dependencies: {} }, null, 2)
2125
1851
  );
2126
1852
  return { success: true, message: "initialized plugins directory" };
@@ -2129,15 +1855,15 @@ var init_plugins = __esm({
2129
1855
  return results;
2130
1856
  }
2131
1857
  results.push({ status: "pass", message: "Plugins directory exists" });
2132
- const pkgPath = path21.join(ctx.pluginsDir, "package.json");
2133
- if (!fs22.existsSync(pkgPath)) {
1858
+ const pkgPath = path20.join(ctx.pluginsDir, "package.json");
1859
+ if (!fs21.existsSync(pkgPath)) {
2134
1860
  results.push({
2135
1861
  status: "warn",
2136
1862
  message: "Plugins package.json missing",
2137
1863
  fixable: true,
2138
1864
  fixRisk: "safe",
2139
1865
  fix: async () => {
2140
- fs22.writeFileSync(
1866
+ fs21.writeFileSync(
2141
1867
  pkgPath,
2142
1868
  JSON.stringify({ name: "openacp-plugins", private: true, dependencies: {} }, null, 2)
2143
1869
  );
@@ -2147,7 +1873,7 @@ var init_plugins = __esm({
2147
1873
  return results;
2148
1874
  }
2149
1875
  try {
2150
- const pkg = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
1876
+ const pkg = JSON.parse(fs21.readFileSync(pkgPath, "utf-8"));
2151
1877
  const deps = pkg.dependencies || {};
2152
1878
  const count = Object.keys(deps).length;
2153
1879
  results.push({ status: "pass", message: `Plugins package.json valid (${count} plugins)` });
@@ -2158,7 +1884,7 @@ var init_plugins = __esm({
2158
1884
  fixable: true,
2159
1885
  fixRisk: "risky",
2160
1886
  fix: async () => {
2161
- fs22.writeFileSync(
1887
+ fs21.writeFileSync(
2162
1888
  pkgPath,
2163
1889
  JSON.stringify({ name: "openacp-plugins", private: true, dependencies: {} }, null, 2)
2164
1890
  );
@@ -2173,7 +1899,7 @@ var init_plugins = __esm({
2173
1899
  });
2174
1900
 
2175
1901
  // src/core/doctor/checks/daemon.ts
2176
- import * as fs23 from "fs";
1902
+ import * as fs22 from "fs";
2177
1903
  import * as net from "net";
2178
1904
  function isProcessAlive(pid) {
2179
1905
  try {
@@ -2203,8 +1929,8 @@ var init_daemon = __esm({
2203
1929
  order: 7,
2204
1930
  async run(ctx) {
2205
1931
  const results = [];
2206
- if (fs23.existsSync(ctx.pidPath)) {
2207
- const content = fs23.readFileSync(ctx.pidPath, "utf-8").trim();
1932
+ if (fs22.existsSync(ctx.pidPath)) {
1933
+ const content = fs22.readFileSync(ctx.pidPath, "utf-8").trim();
2208
1934
  const pid = parseInt(content, 10);
2209
1935
  if (isNaN(pid)) {
2210
1936
  results.push({
@@ -2213,7 +1939,7 @@ var init_daemon = __esm({
2213
1939
  fixable: true,
2214
1940
  fixRisk: "safe",
2215
1941
  fix: async () => {
2216
- fs23.unlinkSync(ctx.pidPath);
1942
+ fs22.unlinkSync(ctx.pidPath);
2217
1943
  return { success: true, message: "removed invalid PID file" };
2218
1944
  }
2219
1945
  });
@@ -2224,7 +1950,7 @@ var init_daemon = __esm({
2224
1950
  fixable: true,
2225
1951
  fixRisk: "safe",
2226
1952
  fix: async () => {
2227
- fs23.unlinkSync(ctx.pidPath);
1953
+ fs22.unlinkSync(ctx.pidPath);
2228
1954
  return { success: true, message: "removed stale PID file" };
2229
1955
  }
2230
1956
  });
@@ -2232,8 +1958,8 @@ var init_daemon = __esm({
2232
1958
  results.push({ status: "pass", message: `Daemon running (PID ${pid})` });
2233
1959
  }
2234
1960
  }
2235
- if (fs23.existsSync(ctx.portFilePath)) {
2236
- const content = fs23.readFileSync(ctx.portFilePath, "utf-8").trim();
1961
+ if (fs22.existsSync(ctx.portFilePath)) {
1962
+ const content = fs22.readFileSync(ctx.portFilePath, "utf-8").trim();
2237
1963
  const port = parseInt(content, 10);
2238
1964
  if (isNaN(port)) {
2239
1965
  results.push({
@@ -2242,7 +1968,7 @@ var init_daemon = __esm({
2242
1968
  fixable: true,
2243
1969
  fixRisk: "safe",
2244
1970
  fix: async () => {
2245
- fs23.unlinkSync(ctx.portFilePath);
1971
+ fs22.unlinkSync(ctx.portFilePath);
2246
1972
  return { success: true, message: "removed invalid port file" };
2247
1973
  }
2248
1974
  });
@@ -2251,11 +1977,11 @@ var init_daemon = __esm({
2251
1977
  }
2252
1978
  }
2253
1979
  if (ctx.config) {
2254
- const apiPort = ctx.config.api.port;
1980
+ const apiPort = 21420;
2255
1981
  const inUse = await checkPortInUse(apiPort);
2256
1982
  if (inUse) {
2257
- if (fs23.existsSync(ctx.pidPath)) {
2258
- const pid = parseInt(fs23.readFileSync(ctx.pidPath, "utf-8").trim(), 10);
1983
+ if (fs22.existsSync(ctx.pidPath)) {
1984
+ const pid = parseInt(fs22.readFileSync(ctx.pidPath, "utf-8").trim(), 10);
2259
1985
  if (!isNaN(pid) && isProcessAlive(pid)) {
2260
1986
  results.push({ status: "pass", message: `API port ${apiPort} in use by OpenACP daemon` });
2261
1987
  } else {
@@ -2275,25 +2001,32 @@ var init_daemon = __esm({
2275
2001
  });
2276
2002
 
2277
2003
  // src/core/utils/install-binary.ts
2278
- import fs24 from "fs";
2279
- import path22 from "path";
2004
+ import fs23 from "fs";
2005
+ import path21 from "path";
2280
2006
  import https from "https";
2281
2007
  import os9 from "os";
2282
- import { execSync } from "child_process";
2283
- function downloadFile(url, dest) {
2008
+ import { execFileSync as execFileSync4 } from "child_process";
2009
+ function downloadFile(url, dest, maxRedirects = 10) {
2284
2010
  return new Promise((resolve6, reject) => {
2285
- const file = fs24.createWriteStream(dest);
2011
+ const file = fs23.createWriteStream(dest);
2286
2012
  const cleanup = () => {
2287
2013
  try {
2288
- if (fs24.existsSync(dest)) fs24.unlinkSync(dest);
2014
+ if (fs23.existsSync(dest)) fs23.unlinkSync(dest);
2289
2015
  } catch {
2290
2016
  }
2291
2017
  };
2292
2018
  https.get(url, (response) => {
2293
2019
  if (response.statusCode === 301 || response.statusCode === 302) {
2020
+ if (maxRedirects <= 0) {
2021
+ file.close(() => {
2022
+ cleanup();
2023
+ reject(new Error("Too many redirects"));
2024
+ });
2025
+ return;
2026
+ }
2294
2027
  file.close(() => {
2295
2028
  cleanup();
2296
- downloadFile(response.headers.location, dest).then(resolve6).catch(reject);
2029
+ downloadFile(response.headers.location, dest, maxRedirects - 1).then(resolve6).catch(reject);
2297
2030
  });
2298
2031
  return;
2299
2032
  }
@@ -2344,36 +2077,36 @@ function getDownloadUrl(spec) {
2344
2077
  async function ensureBinary(spec, binDir) {
2345
2078
  const resolvedBinDir = binDir ?? DEFAULT_BIN_DIR;
2346
2079
  const binName = IS_WINDOWS ? `${spec.name}.exe` : spec.name;
2347
- const binPath = path22.join(resolvedBinDir, binName);
2080
+ const binPath = path21.join(resolvedBinDir, binName);
2348
2081
  if (commandExists(spec.name)) {
2349
2082
  log17.debug({ name: spec.name }, "Found in PATH");
2350
2083
  return spec.name;
2351
2084
  }
2352
- if (fs24.existsSync(binPath)) {
2353
- if (!IS_WINDOWS) fs24.chmodSync(binPath, "755");
2085
+ if (fs23.existsSync(binPath)) {
2086
+ if (!IS_WINDOWS) fs23.chmodSync(binPath, "755");
2354
2087
  log17.debug({ name: spec.name, path: binPath }, "Found in ~/.openacp/bin");
2355
2088
  return binPath;
2356
2089
  }
2357
2090
  log17.info({ name: spec.name }, "Not found, downloading from GitHub...");
2358
- fs24.mkdirSync(resolvedBinDir, { recursive: true });
2091
+ fs23.mkdirSync(resolvedBinDir, { recursive: true });
2359
2092
  const url = getDownloadUrl(spec);
2360
2093
  const isArchive = spec.isArchive?.(url) ?? false;
2361
- const downloadDest = isArchive ? path22.join(resolvedBinDir, `${spec.name}.tgz`) : binPath;
2094
+ const downloadDest = isArchive ? path21.join(resolvedBinDir, `${spec.name}.tgz`) : binPath;
2362
2095
  await downloadFile(url, downloadDest);
2363
2096
  if (isArchive) {
2364
- const listing = execSync(`tar -tf "${downloadDest}"`, { stdio: "pipe" }).toString().trim().split("\n").filter(Boolean);
2097
+ const listing = execFileSync4("tar", ["-tf", downloadDest], { stdio: "pipe" }).toString().trim().split("\n").filter(Boolean);
2365
2098
  validateTarContents(listing, resolvedBinDir);
2366
- execSync(`tar -xzf "${downloadDest}" -C "${resolvedBinDir}"`, { stdio: "pipe" });
2099
+ execFileSync4("tar", ["-xzf", downloadDest, "-C", resolvedBinDir], { stdio: "pipe" });
2367
2100
  try {
2368
- fs24.unlinkSync(downloadDest);
2101
+ fs23.unlinkSync(downloadDest);
2369
2102
  } catch {
2370
2103
  }
2371
2104
  }
2372
- if (!fs24.existsSync(binPath)) {
2105
+ if (!fs23.existsSync(binPath)) {
2373
2106
  throw new Error(`${spec.name}: binary not found at ${binPath} after download/extraction. The archive structure may have changed.`);
2374
2107
  }
2375
2108
  if (!IS_WINDOWS) {
2376
- fs24.chmodSync(binPath, "755");
2109
+ fs23.chmodSync(binPath, "755");
2377
2110
  }
2378
2111
  log17.info({ name: spec.name, path: binPath }, "Installed successfully");
2379
2112
  return binPath;
@@ -2386,7 +2119,7 @@ var init_install_binary = __esm({
2386
2119
  init_agent_dependencies();
2387
2120
  init_agent_installer();
2388
2121
  log17 = createChildLogger({ module: "binary-installer" });
2389
- DEFAULT_BIN_DIR = path22.join(os9.homedir(), ".openacp", "bin");
2122
+ DEFAULT_BIN_DIR = path21.join(os9.homedir(), ".openacp", "bin");
2390
2123
  IS_WINDOWS = os9.platform() === "win32";
2391
2124
  }
2392
2125
  });
@@ -2427,10 +2160,10 @@ var init_install_cloudflared = __esm({
2427
2160
  });
2428
2161
 
2429
2162
  // src/core/doctor/checks/tunnel.ts
2430
- import * as fs25 from "fs";
2431
- import * as path23 from "path";
2163
+ import * as fs24 from "fs";
2164
+ import * as path22 from "path";
2432
2165
  import * as os10 from "os";
2433
- import { execFileSync as execFileSync4 } from "child_process";
2166
+ import { execFileSync as execFileSync5 } from "child_process";
2434
2167
  var tunnelCheck;
2435
2168
  var init_tunnel = __esm({
2436
2169
  "src/core/doctor/checks/tunnel.ts"() {
@@ -2444,21 +2177,26 @@ var init_tunnel = __esm({
2444
2177
  results.push({ status: "fail", message: "Cannot check tunnel \u2014 config not loaded" });
2445
2178
  return results;
2446
2179
  }
2447
- if (!ctx.config.tunnel.enabled) {
2180
+ const { SettingsManager: SettingsManager2 } = await Promise.resolve().then(() => (init_settings_manager(), settings_manager_exports));
2181
+ const sm = new SettingsManager2(path22.join(ctx.pluginsDir, "data"));
2182
+ const tunnelSettings = await sm.loadSettings("@openacp/tunnel");
2183
+ const tunnelEnabled = tunnelSettings.enabled ?? false;
2184
+ const provider = tunnelSettings.provider ?? "cloudflare";
2185
+ const tunnelPort = tunnelSettings.port ?? 3100;
2186
+ if (!tunnelEnabled) {
2448
2187
  results.push({ status: "pass", message: "Tunnel not enabled (skipped)" });
2449
2188
  return results;
2450
2189
  }
2451
- const provider = ctx.config.tunnel.provider;
2452
2190
  results.push({ status: "pass", message: `Tunnel provider: ${provider}` });
2453
2191
  if (provider === "cloudflare") {
2454
2192
  const binName = os10.platform() === "win32" ? "cloudflared.exe" : "cloudflared";
2455
- const binPath = path23.join(ctx.dataDir, "bin", binName);
2193
+ const binPath = path22.join(ctx.dataDir, "bin", binName);
2456
2194
  let found = false;
2457
- if (fs25.existsSync(binPath)) {
2195
+ if (fs24.existsSync(binPath)) {
2458
2196
  found = true;
2459
2197
  } else {
2460
2198
  try {
2461
- execFileSync4("which", ["cloudflared"], { stdio: "pipe" });
2199
+ execFileSync5("which", ["cloudflared"], { stdio: "pipe" });
2462
2200
  found = true;
2463
2201
  } catch {
2464
2202
  }
@@ -2483,7 +2221,6 @@ var init_tunnel = __esm({
2483
2221
  });
2484
2222
  }
2485
2223
  }
2486
- const tunnelPort = ctx.config.tunnel.port;
2487
2224
  if (tunnelPort < 1 || tunnelPort > 65535) {
2488
2225
  results.push({ status: "fail", message: `Invalid tunnel port: ${tunnelPort}` });
2489
2226
  } else {
@@ -2496,8 +2233,8 @@ var init_tunnel = __esm({
2496
2233
  });
2497
2234
 
2498
2235
  // src/core/doctor/index.ts
2499
- import * as fs26 from "fs";
2500
- import * as path24 from "path";
2236
+ import * as fs25 from "fs";
2237
+ import * as path23 from "path";
2501
2238
  var ALL_CHECKS, CHECK_TIMEOUT_MS, DoctorEngine;
2502
2239
  var init_doctor = __esm({
2503
2240
  "src/core/doctor/index.ts"() {
@@ -2579,120 +2316,31 @@ var init_doctor = __esm({
2579
2316
  }
2580
2317
  async buildContext() {
2581
2318
  const dataDir = this.dataDir;
2582
- const configPath = process.env.OPENACP_CONFIG_PATH || path24.join(dataDir, "config.json");
2319
+ const configPath = process.env.OPENACP_CONFIG_PATH || path23.join(dataDir, "config.json");
2583
2320
  let config = null;
2584
2321
  let rawConfig = null;
2585
2322
  try {
2586
- const content = fs26.readFileSync(configPath, "utf-8");
2323
+ const content = fs25.readFileSync(configPath, "utf-8");
2587
2324
  rawConfig = JSON.parse(content);
2588
2325
  const cm = new ConfigManager(configPath);
2589
2326
  await cm.load();
2590
2327
  config = cm.get();
2591
2328
  } catch {
2592
- }
2593
- const logsDir = config ? expandHome3(config.logging.logDir) : path24.join(dataDir, "logs");
2594
- return {
2595
- config,
2596
- rawConfig,
2597
- configPath,
2598
- dataDir,
2599
- sessionsPath: path24.join(dataDir, "sessions.json"),
2600
- pidPath: path24.join(dataDir, "openacp.pid"),
2601
- portFilePath: path24.join(dataDir, "api.port"),
2602
- pluginsDir: path24.join(dataDir, "plugins"),
2603
- logsDir
2604
- };
2605
- }
2606
- };
2607
- }
2608
- });
2609
-
2610
- // src/plugins/telegram/validators.ts
2611
- var validators_exports = {};
2612
- __export(validators_exports, {
2613
- validateBotAdmin: () => validateBotAdmin,
2614
- validateBotToken: () => validateBotToken,
2615
- validateChatId: () => validateChatId
2616
- });
2617
- async function validateBotToken(token) {
2618
- try {
2619
- const res = await fetch(`https://api.telegram.org/bot${token}/getMe`);
2620
- const data = await res.json();
2621
- if (data.ok && data.result) {
2622
- return {
2623
- ok: true,
2624
- botName: data.result.first_name,
2625
- botUsername: data.result.username
2626
- };
2627
- }
2628
- return { ok: false, error: data.description || "Invalid token" };
2629
- } catch (err) {
2630
- return { ok: false, error: err.message };
2631
- }
2632
- }
2633
- async function validateChatId(token, chatId) {
2634
- try {
2635
- const res = await fetch(`https://api.telegram.org/bot${token}/getChat`, {
2636
- method: "POST",
2637
- headers: { "Content-Type": "application/json" },
2638
- body: JSON.stringify({ chat_id: chatId })
2639
- });
2640
- const data = await res.json();
2641
- if (!data.ok || !data.result) {
2642
- return { ok: false, error: data.description || "Invalid chat ID" };
2643
- }
2644
- if (data.result.type !== "supergroup") {
2645
- return {
2646
- ok: false,
2647
- error: `Chat is "${data.result.type}", must be a supergroup`
2648
- };
2649
- }
2650
- return {
2651
- ok: true,
2652
- title: data.result.title,
2653
- isForum: data.result.is_forum === true
2654
- };
2655
- } catch (err) {
2656
- return { ok: false, error: err.message };
2657
- }
2658
- }
2659
- async function validateBotAdmin(token, chatId) {
2660
- try {
2661
- const meRes = await fetch(`https://api.telegram.org/bot${token}/getMe`);
2662
- const meData = await meRes.json();
2663
- if (!meData.ok || !meData.result) {
2664
- return { ok: false, error: "Could not retrieve bot info" };
2665
- }
2666
- const res = await fetch(
2667
- `https://api.telegram.org/bot${token}/getChatMember`,
2668
- {
2669
- method: "POST",
2670
- headers: { "Content-Type": "application/json" },
2671
- body: JSON.stringify({ chat_id: chatId, user_id: meData.result.id })
2329
+ }
2330
+ const logsDir = config ? expandHome3(config.logging.logDir) : path23.join(dataDir, "logs");
2331
+ return {
2332
+ config,
2333
+ rawConfig,
2334
+ configPath,
2335
+ dataDir,
2336
+ sessionsPath: path23.join(dataDir, "sessions.json"),
2337
+ pidPath: path23.join(dataDir, "openacp.pid"),
2338
+ portFilePath: path23.join(dataDir, "api.port"),
2339
+ pluginsDir: path23.join(dataDir, "plugins"),
2340
+ logsDir
2341
+ };
2672
2342
  }
2673
- );
2674
- const data = await res.json();
2675
- if (!data.ok || !data.result) {
2676
- return {
2677
- ok: false,
2678
- error: data.description || "Could not check bot membership"
2679
- };
2680
- }
2681
- const { status } = data.result;
2682
- if (status === "administrator" || status === "creator") {
2683
- return { ok: true };
2684
- }
2685
- return {
2686
- ok: false,
2687
- error: `Bot is "${status}" in this group. It must be an admin. Please promote the bot to admin in group settings.`
2688
2343
  };
2689
- } catch (err) {
2690
- return { ok: false, error: err.message };
2691
- }
2692
- }
2693
- var init_validators = __esm({
2694
- "src/plugins/telegram/validators.ts"() {
2695
- "use strict";
2696
2344
  }
2697
2345
  });
2698
2346
 
@@ -2702,18 +2350,18 @@ __export(plugin_installer_exports, {
2702
2350
  importFromDir: () => importFromDir,
2703
2351
  installNpmPlugin: () => installNpmPlugin
2704
2352
  });
2705
- import { exec } from "child_process";
2353
+ import { execFile } from "child_process";
2706
2354
  import { promisify } from "util";
2707
- import * as fs28 from "fs/promises";
2355
+ import * as fs27 from "fs/promises";
2708
2356
  import * as os12 from "os";
2709
- import * as path26 from "path";
2357
+ import * as path25 from "path";
2710
2358
  import { pathToFileURL } from "url";
2711
2359
  async function importFromDir(packageName, dir) {
2712
- const pkgDir = path26.join(dir, "node_modules", ...packageName.split("/"));
2713
- const pkgJsonPath = path26.join(pkgDir, "package.json");
2360
+ const pkgDir = path25.join(dir, "node_modules", ...packageName.split("/"));
2361
+ const pkgJsonPath = path25.join(pkgDir, "package.json");
2714
2362
  let pkgJson;
2715
2363
  try {
2716
- pkgJson = JSON.parse(await fs28.readFile(pkgJsonPath, "utf-8"));
2364
+ pkgJson = JSON.parse(await fs27.readFile(pkgJsonPath, "utf-8"));
2717
2365
  } catch (err) {
2718
2366
  throw new Error(`Cannot read package.json for "${packageName}" at ${pkgJsonPath}: ${err.message}`);
2719
2367
  }
@@ -2726,9 +2374,9 @@ async function importFromDir(packageName, dir) {
2726
2374
  } else {
2727
2375
  entry = pkgJson.main ?? "index.js";
2728
2376
  }
2729
- const entryPath = path26.join(pkgDir, entry);
2377
+ const entryPath = path25.join(pkgDir, entry);
2730
2378
  try {
2731
- await fs28.access(entryPath);
2379
+ await fs27.access(entryPath);
2732
2380
  } catch {
2733
2381
  throw new Error(`Entry point "${entry}" not found for "${packageName}" at ${entryPath}`);
2734
2382
  }
@@ -2738,21 +2386,21 @@ async function installNpmPlugin(packageName, pluginsDir) {
2738
2386
  if (!VALID_NPM_NAME.test(packageName)) {
2739
2387
  throw new Error(`Invalid package name: "${packageName}". Must be a valid npm package name.`);
2740
2388
  }
2741
- const dir = pluginsDir ?? path26.join(os12.homedir(), ".openacp", "plugins");
2389
+ const dir = pluginsDir ?? path25.join(os12.homedir(), ".openacp", "plugins");
2742
2390
  try {
2743
2391
  return await importFromDir(packageName, dir);
2744
2392
  } catch {
2745
2393
  }
2746
- await execAsync(`npm install ${packageName} --prefix "${dir}" --save --ignore-scripts`, {
2394
+ await execFileAsync("npm", ["install", packageName, "--prefix", dir, "--save", "--ignore-scripts"], {
2747
2395
  timeout: 6e4
2748
2396
  });
2749
2397
  return await importFromDir(packageName, dir);
2750
2398
  }
2751
- var execAsync, VALID_NPM_NAME;
2399
+ var execFileAsync, VALID_NPM_NAME;
2752
2400
  var init_plugin_installer = __esm({
2753
2401
  "src/core/plugin/plugin-installer.ts"() {
2754
2402
  "use strict";
2755
- execAsync = promisify(exec);
2403
+ execFileAsync = promisify(execFile);
2756
2404
  VALID_NPM_NAME = /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*(@[\w.^~>=<|-]+)?$/i;
2757
2405
  }
2758
2406
  });
@@ -2820,15 +2468,14 @@ var install_context_exports = {};
2820
2468
  __export(install_context_exports, {
2821
2469
  createInstallContext: () => createInstallContext
2822
2470
  });
2823
- import path27 from "path";
2471
+ import path26 from "path";
2824
2472
  function createInstallContext(opts) {
2825
- const { pluginName, settingsManager, basePath, legacyConfig, instanceRoot } = opts;
2826
- const dataDir = path27.join(basePath, pluginName, "data");
2473
+ const { pluginName, settingsManager, basePath, instanceRoot } = opts;
2474
+ const dataDir = path26.join(basePath, pluginName, "data");
2827
2475
  return {
2828
2476
  pluginName,
2829
2477
  terminal: createTerminalIO(),
2830
2478
  settings: settingsManager.createAPI(pluginName),
2831
- legacyConfig,
2832
2479
  dataDir,
2833
2480
  log: log.child({ plugin: pluginName }),
2834
2481
  instanceRoot
@@ -2850,19 +2497,19 @@ __export(api_client_exports, {
2850
2497
  readApiSecret: () => readApiSecret,
2851
2498
  removeStalePortFile: () => removeStalePortFile
2852
2499
  });
2853
- import * as fs29 from "fs";
2854
- import * as path28 from "path";
2500
+ import * as fs28 from "fs";
2501
+ import * as path27 from "path";
2855
2502
  import * as os13 from "os";
2856
2503
  function defaultPortFile(root) {
2857
- return path28.join(root ?? DEFAULT_ROOT, "api.port");
2504
+ return path27.join(root ?? DEFAULT_ROOT, "api.port");
2858
2505
  }
2859
2506
  function defaultSecretFile(root) {
2860
- return path28.join(root ?? DEFAULT_ROOT, "api-secret");
2507
+ return path27.join(root ?? DEFAULT_ROOT, "api-secret");
2861
2508
  }
2862
2509
  function readApiPort(portFilePath, instanceRoot) {
2863
2510
  const filePath = portFilePath ?? defaultPortFile(instanceRoot);
2864
2511
  try {
2865
- const content = fs29.readFileSync(filePath, "utf-8").trim();
2512
+ const content = fs28.readFileSync(filePath, "utf-8").trim();
2866
2513
  const port = parseInt(content, 10);
2867
2514
  return isNaN(port) ? null : port;
2868
2515
  } catch {
@@ -2872,7 +2519,7 @@ function readApiPort(portFilePath, instanceRoot) {
2872
2519
  function readApiSecret(secretFilePath, instanceRoot) {
2873
2520
  const filePath = secretFilePath ?? defaultSecretFile(instanceRoot);
2874
2521
  try {
2875
- const content = fs29.readFileSync(filePath, "utf-8").trim();
2522
+ const content = fs28.readFileSync(filePath, "utf-8").trim();
2876
2523
  return content || null;
2877
2524
  } catch {
2878
2525
  return null;
@@ -2881,7 +2528,7 @@ function readApiSecret(secretFilePath, instanceRoot) {
2881
2528
  function removeStalePortFile(portFilePath, instanceRoot) {
2882
2529
  const filePath = portFilePath ?? defaultPortFile(instanceRoot);
2883
2530
  try {
2884
- fs29.unlinkSync(filePath);
2531
+ fs28.unlinkSync(filePath);
2885
2532
  } catch {
2886
2533
  }
2887
2534
  }
@@ -2897,7 +2544,7 @@ var DEFAULT_ROOT;
2897
2544
  var init_api_client = __esm({
2898
2545
  "src/cli/api-client.ts"() {
2899
2546
  "use strict";
2900
- DEFAULT_ROOT = path28.join(os13.homedir(), ".openacp");
2547
+ DEFAULT_ROOT = path27.join(os13.homedir(), ".openacp");
2901
2548
  }
2902
2549
  });
2903
2550
 
@@ -2931,8 +2578,8 @@ var init_notification = __esm({
2931
2578
  });
2932
2579
 
2933
2580
  // src/plugins/file-service/file-service.ts
2934
- import fs31 from "fs";
2935
- import path31 from "path";
2581
+ import fs30 from "fs";
2582
+ import path30 from "path";
2936
2583
  import { OggOpusDecoder } from "ogg-opus-decoder";
2937
2584
  import wav from "node-wav";
2938
2585
  function classifyMime(mimeType) {
@@ -2988,14 +2635,14 @@ var init_file_service = __esm({
2988
2635
  const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
2989
2636
  let removed = 0;
2990
2637
  try {
2991
- const entries = await fs31.promises.readdir(this.baseDir, { withFileTypes: true });
2638
+ const entries = await fs30.promises.readdir(this.baseDir, { withFileTypes: true });
2992
2639
  for (const entry of entries) {
2993
2640
  if (!entry.isDirectory()) continue;
2994
- const dirPath = path31.join(this.baseDir, entry.name);
2641
+ const dirPath = path30.join(this.baseDir, entry.name);
2995
2642
  try {
2996
- const stat = await fs31.promises.stat(dirPath);
2643
+ const stat = await fs30.promises.stat(dirPath);
2997
2644
  if (stat.mtimeMs < cutoff) {
2998
- await fs31.promises.rm(dirPath, { recursive: true, force: true });
2645
+ await fs30.promises.rm(dirPath, { recursive: true, force: true });
2999
2646
  removed++;
3000
2647
  }
3001
2648
  } catch {
@@ -3006,11 +2653,11 @@ var init_file_service = __esm({
3006
2653
  return removed;
3007
2654
  }
3008
2655
  async saveFile(sessionId, fileName, data, mimeType) {
3009
- const sessionDir = path31.join(this.baseDir, sessionId);
3010
- await fs31.promises.mkdir(sessionDir, { recursive: true });
2656
+ const sessionDir = path30.join(this.baseDir, sessionId);
2657
+ await fs30.promises.mkdir(sessionDir, { recursive: true });
3011
2658
  const safeName = `${Date.now()}-${fileName.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
3012
- const filePath = path31.join(sessionDir, safeName);
3013
- await fs31.promises.writeFile(filePath, data);
2659
+ const filePath = path30.join(sessionDir, safeName);
2660
+ await fs30.promises.writeFile(filePath, data);
3014
2661
  return {
3015
2662
  type: classifyMime(mimeType),
3016
2663
  filePath,
@@ -3021,14 +2668,14 @@ var init_file_service = __esm({
3021
2668
  }
3022
2669
  async resolveFile(filePath) {
3023
2670
  try {
3024
- const stat = await fs31.promises.stat(filePath);
2671
+ const stat = await fs30.promises.stat(filePath);
3025
2672
  if (!stat.isFile()) return null;
3026
- const ext = path31.extname(filePath).toLowerCase();
2673
+ const ext = path30.extname(filePath).toLowerCase();
3027
2674
  const mimeType = EXT_TO_MIME[ext] || "application/octet-stream";
3028
2675
  return {
3029
2676
  type: classifyMime(mimeType),
3030
2677
  filePath,
3031
- fileName: path31.basename(filePath),
2678
+ fileName: path30.basename(filePath),
3032
2679
  mimeType,
3033
2680
  size: stat.size
3034
2681
  };
@@ -3240,6 +2887,7 @@ var init_roles = __esm({
3240
2887
  "sessions:prompt",
3241
2888
  "sessions:permission",
3242
2889
  "agents:read",
2890
+ "agents:write",
3243
2891
  "commands:execute",
3244
2892
  "system:health",
3245
2893
  "config:read"
@@ -3358,12 +3006,16 @@ async function createApiServer(options) {
3358
3006
  // per-tunnel. Only the first value in X-Forwarded-For is taken (the client); the
3359
3007
  // rest may be added by intermediate proxies and must not be trusted for limiting.
3360
3008
  keyGenerator: (request) => {
3361
- const cfIp = request.headers["cf-connecting-ip"];
3362
- if (cfIp && typeof cfIp === "string") return cfIp;
3363
- const xff = request.headers["x-forwarded-for"];
3364
- if (xff) {
3365
- const first = (Array.isArray(xff) ? xff[0] : xff).split(",")[0]?.trim();
3366
- if (first) return first;
3009
+ const bindHost = options.host;
3010
+ const isBehindProxy = !bindHost || bindHost === "127.0.0.1" || bindHost === "localhost" || bindHost === "::1";
3011
+ if (isBehindProxy) {
3012
+ const cfIp = request.headers["cf-connecting-ip"];
3013
+ if (cfIp && typeof cfIp === "string") return cfIp;
3014
+ const xff = request.headers["x-forwarded-for"];
3015
+ if (xff) {
3016
+ const first = (Array.isArray(xff) ? xff[0] : xff).split(",")[0]?.trim();
3017
+ if (first) return first;
3018
+ }
3367
3019
  }
3368
3020
  return request.ip;
3369
3021
  }
@@ -3474,7 +3126,10 @@ var init_sse_manager = __esm({
3474
3126
  "session:updated",
3475
3127
  "session:deleted",
3476
3128
  "agent:event",
3477
- "permission:request"
3129
+ "permission:request",
3130
+ "permission:resolved",
3131
+ "message:queued",
3132
+ "message:processing"
3478
3133
  ];
3479
3134
  for (const eventName of events) {
3480
3135
  const handler = (data) => {
@@ -3539,7 +3194,10 @@ data: ${JSON.stringify(data)}
3539
3194
  const sessionEvents = [
3540
3195
  "agent:event",
3541
3196
  "permission:request",
3542
- "session:updated"
3197
+ "permission:resolved",
3198
+ "session:updated",
3199
+ "message:queued",
3200
+ "message:processing"
3543
3201
  ];
3544
3202
  for (const res of this.sseConnections) {
3545
3203
  const filter = res.sessionFilter;
@@ -3582,8 +3240,8 @@ data: ${JSON.stringify(data)}
3582
3240
  });
3583
3241
 
3584
3242
  // src/plugins/api-server/static-server.ts
3585
- import * as fs32 from "fs";
3586
- import * as path32 from "path";
3243
+ import * as fs31 from "fs";
3244
+ import * as path31 from "path";
3587
3245
  import { fileURLToPath } from "url";
3588
3246
  var MIME_TYPES, StaticServer;
3589
3247
  var init_static_server = __esm({
@@ -3607,16 +3265,16 @@ var init_static_server = __esm({
3607
3265
  this.uiDir = uiDir;
3608
3266
  if (!this.uiDir) {
3609
3267
  const __filename = fileURLToPath(import.meta.url);
3610
- const candidate = path32.resolve(path32.dirname(__filename), "../../ui/dist");
3611
- if (fs32.existsSync(path32.join(candidate, "index.html"))) {
3268
+ const candidate = path31.resolve(path31.dirname(__filename), "../../ui/dist");
3269
+ if (fs31.existsSync(path31.join(candidate, "index.html"))) {
3612
3270
  this.uiDir = candidate;
3613
3271
  }
3614
3272
  if (!this.uiDir) {
3615
- const publishCandidate = path32.resolve(
3616
- path32.dirname(__filename),
3273
+ const publishCandidate = path31.resolve(
3274
+ path31.dirname(__filename),
3617
3275
  "../ui"
3618
3276
  );
3619
- if (fs32.existsSync(path32.join(publishCandidate, "index.html"))) {
3277
+ if (fs31.existsSync(path31.join(publishCandidate, "index.html"))) {
3620
3278
  this.uiDir = publishCandidate;
3621
3279
  }
3622
3280
  }
@@ -3628,23 +3286,23 @@ var init_static_server = __esm({
3628
3286
  serve(req, res) {
3629
3287
  if (!this.uiDir) return false;
3630
3288
  const urlPath = (req.url || "/").split("?")[0];
3631
- const safePath = path32.normalize(urlPath);
3632
- const filePath = path32.join(this.uiDir, safePath);
3633
- if (!filePath.startsWith(this.uiDir + path32.sep) && filePath !== this.uiDir)
3289
+ const safePath = path31.normalize(urlPath);
3290
+ const filePath = path31.join(this.uiDir, safePath);
3291
+ if (!filePath.startsWith(this.uiDir + path31.sep) && filePath !== this.uiDir)
3634
3292
  return false;
3635
3293
  let realFilePath;
3636
3294
  try {
3637
- realFilePath = fs32.realpathSync(filePath);
3295
+ realFilePath = fs31.realpathSync(filePath);
3638
3296
  } catch {
3639
3297
  realFilePath = null;
3640
3298
  }
3641
3299
  if (realFilePath !== null) {
3642
- const realUiDir = fs32.realpathSync(this.uiDir);
3643
- if (!realFilePath.startsWith(realUiDir + path32.sep) && realFilePath !== realUiDir)
3300
+ const realUiDir = fs31.realpathSync(this.uiDir);
3301
+ if (!realFilePath.startsWith(realUiDir + path31.sep) && realFilePath !== realUiDir)
3644
3302
  return false;
3645
3303
  }
3646
- if (realFilePath !== null && fs32.existsSync(realFilePath) && fs32.statSync(realFilePath).isFile()) {
3647
- const ext = path32.extname(filePath);
3304
+ if (realFilePath !== null && fs31.existsSync(realFilePath) && fs31.statSync(realFilePath).isFile()) {
3305
+ const ext = path31.extname(filePath);
3648
3306
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
3649
3307
  const isHashed = /\.[a-zA-Z0-9]{8,}\.(js|css)$/.test(filePath);
3650
3308
  const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "no-cache";
@@ -3652,16 +3310,16 @@ var init_static_server = __esm({
3652
3310
  "Content-Type": contentType,
3653
3311
  "Cache-Control": cacheControl
3654
3312
  });
3655
- fs32.createReadStream(realFilePath).pipe(res);
3313
+ fs31.createReadStream(realFilePath).pipe(res);
3656
3314
  return true;
3657
3315
  }
3658
- const indexPath = path32.join(this.uiDir, "index.html");
3659
- if (fs32.existsSync(indexPath)) {
3316
+ const indexPath = path31.join(this.uiDir, "index.html");
3317
+ if (fs31.existsSync(indexPath)) {
3660
3318
  res.writeHead(200, {
3661
3319
  "Content-Type": "text/html; charset=utf-8",
3662
3320
  "Cache-Control": "no-cache"
3663
3321
  });
3664
- fs32.createReadStream(indexPath).pipe(res);
3322
+ fs31.createReadStream(indexPath).pipe(res);
3665
3323
  return true;
3666
3324
  }
3667
3325
  return false;
@@ -3816,8 +3474,8 @@ var init_exports = __esm({
3816
3474
  });
3817
3475
 
3818
3476
  // src/plugins/context/context-cache.ts
3819
- import * as fs33 from "fs";
3820
- import * as path33 from "path";
3477
+ import * as fs32 from "fs";
3478
+ import * as path32 from "path";
3821
3479
  import * as crypto2 from "crypto";
3822
3480
  var DEFAULT_TTL_MS, ContextCache;
3823
3481
  var init_context_cache = __esm({
@@ -3828,29 +3486,29 @@ var init_context_cache = __esm({
3828
3486
  constructor(cacheDir, ttlMs = DEFAULT_TTL_MS) {
3829
3487
  this.cacheDir = cacheDir;
3830
3488
  this.ttlMs = ttlMs;
3831
- fs33.mkdirSync(cacheDir, { recursive: true });
3489
+ fs32.mkdirSync(cacheDir, { recursive: true });
3832
3490
  }
3833
3491
  keyHash(repoPath, queryKey) {
3834
3492
  return crypto2.createHash("sha256").update(`${repoPath}:${queryKey}`).digest("hex").slice(0, 16);
3835
3493
  }
3836
3494
  filePath(repoPath, queryKey) {
3837
- return path33.join(this.cacheDir, `${this.keyHash(repoPath, queryKey)}.json`);
3495
+ return path32.join(this.cacheDir, `${this.keyHash(repoPath, queryKey)}.json`);
3838
3496
  }
3839
3497
  get(repoPath, queryKey) {
3840
3498
  const fp = this.filePath(repoPath, queryKey);
3841
3499
  try {
3842
- const stat = fs33.statSync(fp);
3500
+ const stat = fs32.statSync(fp);
3843
3501
  if (Date.now() - stat.mtimeMs > this.ttlMs) {
3844
- fs33.unlinkSync(fp);
3502
+ fs32.unlinkSync(fp);
3845
3503
  return null;
3846
3504
  }
3847
- return JSON.parse(fs33.readFileSync(fp, "utf-8"));
3505
+ return JSON.parse(fs32.readFileSync(fp, "utf-8"));
3848
3506
  } catch {
3849
3507
  return null;
3850
3508
  }
3851
3509
  }
3852
3510
  set(repoPath, queryKey, result) {
3853
- fs33.writeFileSync(this.filePath(repoPath, queryKey), JSON.stringify(result));
3511
+ fs32.writeFileSync(this.filePath(repoPath, queryKey), JSON.stringify(result));
3854
3512
  }
3855
3513
  };
3856
3514
  }
@@ -3858,7 +3516,7 @@ var init_context_cache = __esm({
3858
3516
 
3859
3517
  // src/plugins/context/context-manager.ts
3860
3518
  import * as os15 from "os";
3861
- import * as path34 from "path";
3519
+ import * as path33 from "path";
3862
3520
  var ContextManager;
3863
3521
  var init_context_manager = __esm({
3864
3522
  "src/plugins/context/context-manager.ts"() {
@@ -3868,12 +3526,25 @@ var init_context_manager = __esm({
3868
3526
  providers = [];
3869
3527
  cache;
3870
3528
  historyStore;
3529
+ sessionFlusher;
3871
3530
  constructor(cachePath) {
3872
- this.cache = new ContextCache(cachePath ?? path34.join(os15.homedir(), ".openacp", "cache", "entire"));
3531
+ this.cache = new ContextCache(cachePath ?? path33.join(os15.homedir(), ".openacp", "cache", "entire"));
3873
3532
  }
3874
3533
  setHistoryStore(store) {
3875
3534
  this.historyStore = store;
3876
3535
  }
3536
+ /** Register a callback that flushes in-memory recorder state for a session to disk. */
3537
+ registerFlusher(fn) {
3538
+ this.sessionFlusher = fn;
3539
+ }
3540
+ /**
3541
+ * Flush the recorder state for a session to disk before reading its context.
3542
+ * Call this before buildContext() when switching agents to avoid a race
3543
+ * where the last turn hasn't been persisted yet.
3544
+ */
3545
+ async flushSession(sessionId) {
3546
+ if (this.sessionFlusher) await this.sessionFlusher(sessionId);
3547
+ }
3877
3548
  async getHistory(sessionId) {
3878
3549
  if (!this.historyStore) return null;
3879
3550
  return this.historyStore.read(sessionId);
@@ -3897,13 +3568,17 @@ var init_context_manager = __esm({
3897
3568
  }
3898
3569
  async buildContext(query, options) {
3899
3570
  const queryKey = `${query.type}:${query.value}:${options?.limit ?? ""}:${options?.maxTokens ?? ""}:${options?.labelAgent ?? ""}`;
3900
- const cached = this.cache.get(query.repoPath, queryKey);
3901
- if (cached) return cached;
3571
+ if (!options?.noCache) {
3572
+ const cached = this.cache.get(query.repoPath, queryKey);
3573
+ if (cached) return cached;
3574
+ }
3902
3575
  for (const provider of this.providers) {
3903
3576
  if (!await provider.isAvailable(query.repoPath)) continue;
3904
3577
  const result = await provider.buildContext(query, options);
3905
3578
  if (result && result.markdown) {
3906
- this.cache.set(query.repoPath, queryKey, result);
3579
+ if (!options?.noCache) {
3580
+ this.cache.set(query.repoPath, queryKey, result);
3581
+ }
3907
3582
  return result;
3908
3583
  }
3909
3584
  }
@@ -3924,7 +3599,7 @@ var init_context_provider = __esm({
3924
3599
  });
3925
3600
 
3926
3601
  // src/plugins/context/entire/checkpoint-reader.ts
3927
- import { execFileSync as execFileSync6 } from "child_process";
3602
+ import { execFileSync as execFileSync7 } from "child_process";
3928
3603
  var ENTIRE_BRANCH, CHECKPOINT_ID_RE, SESSION_ID_RE, CheckpointReader;
3929
3604
  var init_checkpoint_reader = __esm({
3930
3605
  "src/plugins/context/entire/checkpoint-reader.ts"() {
@@ -3943,7 +3618,7 @@ var init_checkpoint_reader = __esm({
3943
3618
  */
3944
3619
  git(...args) {
3945
3620
  try {
3946
- return execFileSync6("git", ["-C", this.repoPath, ...args], {
3621
+ return execFileSync7("git", ["-C", this.repoPath, ...args], {
3947
3622
  encoding: "utf-8"
3948
3623
  }).trim();
3949
3624
  } catch {
@@ -4770,8 +4445,8 @@ function formatToolSummary(name, rawInput, displaySummary) {
4770
4445
  }
4771
4446
  if (lowerName === "grep") {
4772
4447
  const pattern = args.pattern ?? "";
4773
- const path35 = args.path ?? "";
4774
- return pattern ? `\u{1F50D} Grep "${pattern}"${path35 ? ` in ${path35}` : ""}` : `\u{1F527} ${name}`;
4448
+ const path34 = args.path ?? "";
4449
+ return pattern ? `\u{1F50D} Grep "${pattern}"${path34 ? ` in ${path34}` : ""}` : `\u{1F527} ${name}`;
4775
4450
  }
4776
4451
  if (lowerName === "glob") {
4777
4452
  const pattern = args.pattern ?? "";
@@ -4807,8 +4482,8 @@ function formatToolTitle(name, rawInput, displayTitle) {
4807
4482
  }
4808
4483
  if (lowerName === "grep") {
4809
4484
  const pattern = args.pattern ?? "";
4810
- const path35 = args.path ?? "";
4811
- return pattern ? `"${pattern}"${path35 ? ` in ${path35}` : ""}` : name;
4485
+ const path34 = args.path ?? "";
4486
+ return pattern ? `"${pattern}"${path34 ? ` in ${path34}` : ""}` : name;
4812
4487
  }
4813
4488
  if (lowerName === "glob") {
4814
4489
  return String(args.pattern ?? name);
@@ -5486,12 +5161,34 @@ function asRecord(value) {
5486
5161
  function capitalize(s) {
5487
5162
  return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);
5488
5163
  }
5164
+ function getStringField(input2, keys) {
5165
+ for (const key of keys) {
5166
+ const value = input2[key];
5167
+ if (typeof value === "string" && value.trim().length > 0) return value;
5168
+ }
5169
+ return null;
5170
+ }
5171
+ function parseApplyPatchTargets(patchText) {
5172
+ const targets = [];
5173
+ const seen = /* @__PURE__ */ new Set();
5174
+ for (const line of patchText.split("\n")) {
5175
+ const match = line.match(/^\*\*\*\s+(?:Update|Add|Delete)\s+File:\s+(.+)$/);
5176
+ if (!match) continue;
5177
+ const p = match[1].trim();
5178
+ if (p && !seen.has(p)) {
5179
+ seen.add(p);
5180
+ targets.push(p);
5181
+ }
5182
+ }
5183
+ return targets;
5184
+ }
5489
5185
  function buildTitle(entry, kind) {
5490
5186
  if (entry.displayTitle) return entry.displayTitle;
5491
5187
  if (entry.displaySummary) return entry.displaySummary;
5492
5188
  const input2 = asRecord(entry.rawInput);
5189
+ const nameLower = entry.name.toLowerCase();
5493
5190
  if (kind === "read") {
5494
- const filePath = typeof input2.file_path === "string" ? input2.file_path : null;
5191
+ const filePath = getStringField(input2, ["file_path", "filePath", "path"]);
5495
5192
  if (filePath) {
5496
5193
  const startLine = typeof input2.start_line === "number" ? input2.start_line : null;
5497
5194
  const endLine = typeof input2.end_line === "number" ? input2.end_line : null;
@@ -5506,7 +5203,7 @@ function buildTitle(entry, kind) {
5506
5203
  return capitalize(entry.name);
5507
5204
  }
5508
5205
  if (kind === "edit" || kind === "write" || kind === "delete") {
5509
- const filePath = typeof input2.file_path === "string" ? input2.file_path : typeof input2.path === "string" ? input2.path : null;
5206
+ const filePath = getStringField(input2, ["file_path", "filePath", "path"]);
5510
5207
  if (filePath) return filePath;
5511
5208
  return capitalize(entry.name);
5512
5209
  }
@@ -5538,6 +5235,36 @@ function buildTitle(entry, kind) {
5538
5235
  }
5539
5236
  return capitalize(entry.name);
5540
5237
  }
5238
+ if (nameLower === "apply_patch") {
5239
+ const patchText = getStringField(input2, ["patchText", "patch_text"]);
5240
+ if (patchText) {
5241
+ const targets = parseApplyPatchTargets(patchText);
5242
+ if (targets.length === 1) return targets[0];
5243
+ if (targets.length > 1) {
5244
+ const shown = targets.slice(0, 2).join(", ");
5245
+ const remaining = targets.length - 2;
5246
+ return remaining > 0 ? `${shown} (+${remaining} more)` : shown;
5247
+ }
5248
+ }
5249
+ return "apply_patch";
5250
+ }
5251
+ if (nameLower === "todowrite") {
5252
+ const todos = Array.isArray(input2.todos) ? input2.todos : [];
5253
+ if (todos.length > 0) {
5254
+ const inProgress = todos.filter((t) => {
5255
+ if (!t || typeof t !== "object") return false;
5256
+ const status = t.status;
5257
+ return status === "in_progress";
5258
+ }).length;
5259
+ const completed = todos.filter((t) => {
5260
+ if (!t || typeof t !== "object") return false;
5261
+ const status = t.status;
5262
+ return status === "completed";
5263
+ }).length;
5264
+ return `Todo list (${completed}/${todos.length} done${inProgress > 0 ? `, ${inProgress} active` : ""})`;
5265
+ }
5266
+ return "Todo list";
5267
+ }
5541
5268
  if (kind === "fetch" || kind === "web") {
5542
5269
  const url = typeof input2.url === "string" ? input2.url : null;
5543
5270
  if (url && url !== "undefined") return url.length > 60 ? url.slice(0, 57) + "..." : url;
@@ -5545,11 +5272,36 @@ function buildTitle(entry, kind) {
5545
5272
  if (query && query !== "undefined") return query.length > 60 ? query.slice(0, 57) + "..." : query;
5546
5273
  return capitalize(entry.name);
5547
5274
  }
5548
- if (entry.name.toLowerCase() === "skill" && typeof input2.skill === "string" && input2.skill) {
5275
+ if (nameLower === "skill" && typeof input2.skill === "string" && input2.skill) {
5549
5276
  return input2.skill;
5550
5277
  }
5278
+ if (nameLower === "apply_patch") {
5279
+ const patchText = getStringField(input2, ["patchText", "patch_text"]);
5280
+ if (patchText) {
5281
+ const targets = parseApplyPatchTargets(patchText);
5282
+ if (targets.length === 1) return targets[0];
5283
+ if (targets.length > 1) {
5284
+ const shown = targets.slice(0, 2).join(", ");
5285
+ const rest = targets.length - 2;
5286
+ return rest > 0 ? `${shown} (+${rest} more)` : shown;
5287
+ }
5288
+ }
5289
+ return "apply_patch";
5290
+ }
5291
+ if (nameLower === "todowrite") {
5292
+ const todos = Array.isArray(input2.todos) ? input2.todos : [];
5293
+ if (todos.length > 0) {
5294
+ const completed = todos.filter((t) => isRecord(t) && t.status === "completed").length;
5295
+ const active = todos.filter((t) => isRecord(t) && t.status === "in_progress").length;
5296
+ return `Todo list (${completed}/${todos.length} done${active > 0 ? `, ${active} active` : ""})`;
5297
+ }
5298
+ return "Todo list";
5299
+ }
5551
5300
  return entry.name;
5552
5301
  }
5302
+ function isRecord(value) {
5303
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5304
+ }
5553
5305
  function buildOutputSummary(content) {
5554
5306
  const lines = content.split("\n").length;
5555
5307
  return `${lines} line${lines === 1 ? "" : "s"} of output`;
@@ -5624,6 +5376,7 @@ var init_display_spec_builder = __esm({
5624
5376
  viewerLinks: entry.viewerLinks,
5625
5377
  outputViewerLink,
5626
5378
  outputFallbackContent,
5379
+ workingDirectory: sessionContext?.workingDirectory,
5627
5380
  status: entry.status,
5628
5381
  isNoise: entry.isNoise,
5629
5382
  isHidden
@@ -5854,20 +5607,33 @@ function renderToolCard(snap) {
5854
5607
  }
5855
5608
  return sections.join("\n\n");
5856
5609
  }
5857
- function shortenTitle(title, kind) {
5858
- if (!FILE_KINDS.has(kind) || !title.includes("/")) return title;
5610
+ function normalizePathLike(pathLike) {
5611
+ return pathLike.replace(/\\/g, "/");
5612
+ }
5613
+ function shortenTitle(title, kind, workingDirectory) {
5614
+ if (!title.includes("/")) return title;
5859
5615
  const parenIdx = title.indexOf(" (");
5860
5616
  const pathPart = parenIdx > 0 ? title.slice(0, parenIdx) : title;
5861
5617
  const rangePart = parenIdx > 0 ? title.slice(parenIdx) : "";
5862
- const fileName = pathPart.split("/").pop() || pathPart;
5863
- return fileName + rangePart;
5618
+ if (workingDirectory) {
5619
+ const normalizedPathPart = normalizePathLike(pathPart);
5620
+ const normalizedCwd = normalizePathLike(workingDirectory).replace(/\/+$/, "");
5621
+ const prefix = `${normalizedCwd}/`;
5622
+ const relativized = normalizedPathPart.split(", ").map((segment) => segment.startsWith(prefix) ? segment.slice(prefix.length) : segment).join(", ");
5623
+ if (relativized !== normalizedPathPart) return relativized + rangePart;
5624
+ }
5625
+ if (FILE_KINDS.has(kind)) return basename(pathPart) + rangePart;
5626
+ return title;
5627
+ }
5628
+ function basename(pathLike) {
5629
+ return pathLike.replace(/\\/g, "/").split("/").pop() || pathLike;
5864
5630
  }
5865
5631
  function renderSpecSection(spec) {
5866
5632
  const lines = [];
5867
5633
  const DONE = /* @__PURE__ */ new Set(["completed", "done", "failed", "error"]);
5868
5634
  const statusPrefix = spec.status === "error" || spec.status === "failed" ? "\u274C " : DONE.has(spec.status) ? "\u2705 " : "\u{1F504} ";
5869
5635
  const kindLabel = KIND_LABELS[spec.kind];
5870
- const displayTitle = shortenTitle(spec.title, spec.kind);
5636
+ const displayTitle = shortenTitle(spec.title, spec.kind, spec.workingDirectory);
5871
5637
  const hasUniqueTitle = displayTitle && displayTitle.toLowerCase() !== kindLabel?.toLowerCase() && displayTitle.toLowerCase() !== spec.kind;
5872
5638
  let titleLine;
5873
5639
  if (kindLabel) {
@@ -5896,9 +5662,9 @@ function renderSpecSection(spec) {
5896
5662
  }
5897
5663
  if (spec.viewerLinks?.file || spec.viewerLinks?.diff || spec.outputViewerLink) {
5898
5664
  const linkParts = [];
5899
- const shortName = displayTitle || kindLabel || spec.kind;
5665
+ const linkName = basename(displayTitle || kindLabel || spec.kind);
5900
5666
  if (spec.viewerLinks?.file)
5901
- linkParts.push(`<a href="${escapeHtml(spec.viewerLinks.file)}">View ${escapeHtml(shortName)}</a>`);
5667
+ linkParts.push(`<a href="${escapeHtml(spec.viewerLinks.file)}">View ${escapeHtml(linkName)}</a>`);
5902
5668
  if (spec.viewerLinks?.diff)
5903
5669
  linkParts.push(`<a href="${escapeHtml(spec.viewerLinks.diff)}">View diff</a>`);
5904
5670
  if (spec.outputViewerLink)
@@ -5949,13 +5715,13 @@ __export(version_exports, {
5949
5715
  runUpdate: () => runUpdate
5950
5716
  });
5951
5717
  import { fileURLToPath as fileURLToPath2 } from "url";
5952
- import { dirname as dirname11, join as join21, resolve as resolve5 } from "path";
5953
- import { existsSync as existsSync18, readFileSync as readFileSync14 } from "fs";
5718
+ import { dirname as dirname10, join as join20, resolve as resolve5 } from "path";
5719
+ import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
5954
5720
  function findPackageJson() {
5955
- let dir = dirname11(fileURLToPath2(import.meta.url));
5721
+ let dir = dirname10(fileURLToPath2(import.meta.url));
5956
5722
  for (let i = 0; i < 5; i++) {
5957
- const candidate = join21(dir, "package.json");
5958
- if (existsSync18(candidate)) return candidate;
5723
+ const candidate = join20(dir, "package.json");
5724
+ if (existsSync17(candidate)) return candidate;
5959
5725
  const parent = resolve5(dir, "..");
5960
5726
  if (parent === dir) break;
5961
5727
  dir = parent;
@@ -5966,7 +5732,7 @@ function getCurrentVersion() {
5966
5732
  try {
5967
5733
  const pkgPath = findPackageJson();
5968
5734
  if (!pkgPath) return "0.0.0-dev";
5969
- const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
5735
+ const pkg = JSON.parse(readFileSync15(pkgPath, "utf-8"));
5970
5736
  return pkg.version;
5971
5737
  } catch {
5972
5738
  return "0.0.0-dev";
@@ -6343,11 +6109,16 @@ async function createSessionDirect(ctx, core, chatId, agentName, workspace, onCo
6343
6109
  }
6344
6110
  function cacheWorkspace(agentKey, workspace) {
6345
6111
  const now = Date.now();
6112
+ for (const [id2, entry] of workspaceCache) {
6113
+ if (now - entry.ts > 5 * 6e4) {
6114
+ workspaceCache.delete(id2);
6115
+ }
6116
+ }
6346
6117
  if (workspaceCache.size > WS_CACHE_MAX) {
6347
- for (const [id2, entry] of workspaceCache) {
6348
- if (now - entry.ts > 5 * 6e4 || workspaceCache.size > WS_CACHE_MAX) {
6349
- workspaceCache.delete(id2);
6350
- }
6118
+ const sorted = [...workspaceCache.entries()].sort((a, b) => a[1].ts - b[1].ts);
6119
+ const toDelete = sorted.slice(0, workspaceCache.size - WS_CACHE_MAX);
6120
+ for (const [id2] of toDelete) {
6121
+ workspaceCache.delete(id2);
6351
6122
  }
6352
6123
  }
6353
6124
  const id = nextWsId++;
@@ -6789,9 +6560,12 @@ __export(integrate_exports, {
6789
6560
  listIntegrations: () => listIntegrations,
6790
6561
  uninstallIntegration: () => uninstallIntegration
6791
6562
  });
6792
- import { existsSync as existsSync19, mkdirSync as mkdirSync12, readFileSync as readFileSync15, writeFileSync as writeFileSync12, unlinkSync as unlinkSync7, chmodSync, rmdirSync } from "fs";
6793
- import { join as join22, dirname as dirname12 } from "path";
6563
+ import { existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync11, unlinkSync as unlinkSync7, chmodSync, rmdirSync } from "fs";
6564
+ import { join as join21, dirname as dirname11 } from "path";
6794
6565
  import { homedir as homedir10 } from "os";
6566
+ function isHooksIntegrationSpec(spec) {
6567
+ return spec.strategy === "hooks";
6568
+ }
6795
6569
  function expandPath(p) {
6796
6570
  return p.replace(/^~/, homedir10());
6797
6571
  }
@@ -6909,12 +6683,48 @@ Examples:
6909
6683
  /openacp:handoff telegram
6910
6684
  `;
6911
6685
  }
6686
+ function generateOpencodeHandoffCommand(spec) {
6687
+ return `---
6688
+ name: ${spec.handoffCommandName}
6689
+ description: Transfer current OpenCode session to OpenACP (Telegram/Discord)
6690
+ ---
6691
+
6692
+ Use OPENCODE_SESSION_ID from injected context, then run:
6693
+
6694
+ openacp adopt opencode <OPENCODE_SESSION_ID>
6695
+
6696
+ If a channel argument is provided, append:
6697
+
6698
+ --channel <channel_name>
6699
+
6700
+ Usage:
6701
+ /${spec.handoffCommandName}
6702
+ /${spec.handoffCommandName} telegram
6703
+ `;
6704
+ }
6705
+ function generateOpencodePlugin(spec) {
6706
+ return `export const OpenACPHandoffPlugin = async () => {
6707
+ return {
6708
+ "command.execute.before": async (input, output) => {
6709
+ if (input.command !== ${JSON.stringify(spec.handoffCommandName)}) return
6710
+ output.parts.unshift({
6711
+ id: "openacp-session-inject",
6712
+ sessionID: input.sessionID,
6713
+ messageID: "openacp-inject",
6714
+ type: "text",
6715
+ text: \`OPENCODE_SESSION_ID: \${input.sessionID}\\n\`,
6716
+ })
6717
+ },
6718
+ }
6719
+ }
6720
+ `;
6721
+ }
6912
6722
  function mergeSettingsJson(settingsPath, hookEvent, hookScriptPath) {
6913
6723
  const fullPath = expandPath(settingsPath);
6914
6724
  let settings = {};
6915
- if (existsSync19(fullPath)) {
6916
- const raw = readFileSync15(fullPath, "utf-8");
6917
- writeFileSync12(`${fullPath}.bak`, raw);
6725
+ if (existsSync18(fullPath)) {
6726
+ const raw = readFileSync16(fullPath, "utf-8");
6727
+ writeFileSync11(`${fullPath}.bak`, raw);
6918
6728
  settings = JSON.parse(raw);
6919
6729
  }
6920
6730
  const hooks = settings.hooks ?? {};
@@ -6929,15 +6739,15 @@ function mergeSettingsJson(settingsPath, hookEvent, hookScriptPath) {
6929
6739
  hooks: [{ type: "command", command: hookScriptPath }]
6930
6740
  });
6931
6741
  }
6932
- mkdirSync12(dirname12(fullPath), { recursive: true });
6933
- writeFileSync12(fullPath, JSON.stringify(settings, null, 2) + "\n");
6742
+ mkdirSync11(dirname11(fullPath), { recursive: true });
6743
+ writeFileSync11(fullPath, JSON.stringify(settings, null, 2) + "\n");
6934
6744
  }
6935
6745
  function mergeHooksJson(settingsPath, hookEvent, hookScriptPath) {
6936
6746
  const fullPath = expandPath(settingsPath);
6937
6747
  let config = { version: 1 };
6938
- if (existsSync19(fullPath)) {
6939
- const raw = readFileSync15(fullPath, "utf-8");
6940
- writeFileSync12(`${fullPath}.bak`, raw);
6748
+ if (existsSync18(fullPath)) {
6749
+ const raw = readFileSync16(fullPath, "utf-8");
6750
+ writeFileSync11(`${fullPath}.bak`, raw);
6941
6751
  config = JSON.parse(raw);
6942
6752
  }
6943
6753
  const hooks = config.hooks ?? {};
@@ -6948,13 +6758,13 @@ function mergeHooksJson(settingsPath, hookEvent, hookScriptPath) {
6948
6758
  if (!alreadyInstalled) {
6949
6759
  eventHooks.push({ command: hookScriptPath });
6950
6760
  }
6951
- mkdirSync12(dirname12(fullPath), { recursive: true });
6952
- writeFileSync12(fullPath, JSON.stringify(config, null, 2) + "\n");
6761
+ mkdirSync11(dirname11(fullPath), { recursive: true });
6762
+ writeFileSync11(fullPath, JSON.stringify(config, null, 2) + "\n");
6953
6763
  }
6954
6764
  function removeFromSettingsJson(settingsPath, hookEvent) {
6955
6765
  const fullPath = expandPath(settingsPath);
6956
- if (!existsSync19(fullPath)) return;
6957
- const raw = readFileSync15(fullPath, "utf-8");
6766
+ if (!existsSync18(fullPath)) return;
6767
+ const raw = readFileSync16(fullPath, "utf-8");
6958
6768
  const settings = JSON.parse(raw);
6959
6769
  const hooks = settings.hooks;
6960
6770
  if (!hooks?.[hookEvent]) return;
@@ -6964,12 +6774,12 @@ function removeFromSettingsJson(settingsPath, hookEvent) {
6964
6774
  if (hooks[hookEvent].length === 0) {
6965
6775
  delete hooks[hookEvent];
6966
6776
  }
6967
- writeFileSync12(fullPath, JSON.stringify(settings, null, 2) + "\n");
6777
+ writeFileSync11(fullPath, JSON.stringify(settings, null, 2) + "\n");
6968
6778
  }
6969
6779
  function removeFromHooksJson(settingsPath, hookEvent) {
6970
6780
  const fullPath = expandPath(settingsPath);
6971
- if (!existsSync19(fullPath)) return;
6972
- const raw = readFileSync15(fullPath, "utf-8");
6781
+ if (!existsSync18(fullPath)) return;
6782
+ const raw = readFileSync16(fullPath, "utf-8");
6973
6783
  const config = JSON.parse(raw);
6974
6784
  const hooks = config.hooks;
6975
6785
  if (!hooks?.[hookEvent]) return;
@@ -6979,9 +6789,9 @@ function removeFromHooksJson(settingsPath, hookEvent) {
6979
6789
  if (hooks[hookEvent].length === 0) {
6980
6790
  delete hooks[hookEvent];
6981
6791
  }
6982
- writeFileSync12(fullPath, JSON.stringify(config, null, 2) + "\n");
6792
+ writeFileSync11(fullPath, JSON.stringify(config, null, 2) + "\n");
6983
6793
  }
6984
- async function installIntegration(agentKey, spec) {
6794
+ async function installHooksIntegration(agentKey, spec) {
6985
6795
  const logs = [];
6986
6796
  try {
6987
6797
  if (!commandExists("jq")) {
@@ -6991,31 +6801,31 @@ async function installIntegration(agentKey, spec) {
6991
6801
  };
6992
6802
  }
6993
6803
  const hooksDir = expandPath(spec.hooksDirPath);
6994
- mkdirSync12(hooksDir, { recursive: true });
6995
- const injectPath = join22(hooksDir, "openacp-inject-session.sh");
6996
- writeFileSync12(injectPath, generateInjectScript(agentKey, spec));
6804
+ mkdirSync11(hooksDir, { recursive: true });
6805
+ const injectPath = join21(hooksDir, "openacp-inject-session.sh");
6806
+ writeFileSync11(injectPath, generateInjectScript(agentKey, spec));
6997
6807
  chmodSync(injectPath, 493);
6998
6808
  logs.push(`Created ${injectPath}`);
6999
- const handoffPath = join22(hooksDir, "openacp-handoff.sh");
7000
- writeFileSync12(handoffPath, generateHandoffScript(agentKey));
6809
+ const handoffPath = join21(hooksDir, "openacp-handoff.sh");
6810
+ writeFileSync11(handoffPath, generateHandoffScript(agentKey));
7001
6811
  chmodSync(handoffPath, 493);
7002
6812
  logs.push(`Created ${handoffPath}`);
7003
6813
  if (spec.commandsPath && spec.handoffCommandName) {
7004
6814
  if (spec.commandFormat === "skill") {
7005
- const skillDir = expandPath(join22(spec.commandsPath, spec.handoffCommandName));
7006
- mkdirSync12(skillDir, { recursive: true });
7007
- const skillPath = join22(skillDir, "SKILL.md");
7008
- writeFileSync12(skillPath, generateHandoffCommand(agentKey, spec));
6815
+ const skillDir = expandPath(join21(spec.commandsPath, spec.handoffCommandName));
6816
+ mkdirSync11(skillDir, { recursive: true });
6817
+ const skillPath = join21(skillDir, "SKILL.md");
6818
+ writeFileSync11(skillPath, generateHandoffCommand(agentKey, spec));
7009
6819
  logs.push(`Created ${skillPath}`);
7010
6820
  } else {
7011
6821
  const cmdsDir = expandPath(spec.commandsPath);
7012
- mkdirSync12(cmdsDir, { recursive: true });
7013
- const cmdPath = join22(cmdsDir, `${spec.handoffCommandName}.md`);
7014
- writeFileSync12(cmdPath, generateHandoffCommand(agentKey, spec));
6822
+ mkdirSync11(cmdsDir, { recursive: true });
6823
+ const cmdPath = join21(cmdsDir, `${spec.handoffCommandName}.md`);
6824
+ writeFileSync11(cmdPath, generateHandoffCommand(agentKey, spec));
7015
6825
  logs.push(`Created ${cmdPath}`);
7016
6826
  }
7017
6827
  }
7018
- const injectFullPath = join22(hooksDir, "openacp-inject-session.sh");
6828
+ const injectFullPath = join21(hooksDir, "openacp-inject-session.sh");
7019
6829
  if (spec.settingsFormat === "hooks_json") {
7020
6830
  mergeHooksJson(spec.settingsPath, spec.hookEvent, injectFullPath);
7021
6831
  } else {
@@ -7028,22 +6838,22 @@ async function installIntegration(agentKey, spec) {
7028
6838
  return { success: false, logs };
7029
6839
  }
7030
6840
  }
7031
- async function uninstallIntegration(agentKey, spec) {
6841
+ async function uninstallHooksIntegration(agentKey, spec) {
7032
6842
  const logs = [];
7033
6843
  try {
7034
6844
  const hooksDir = expandPath(spec.hooksDirPath);
7035
6845
  for (const filename of ["openacp-inject-session.sh", "openacp-handoff.sh"]) {
7036
- const filePath = join22(hooksDir, filename);
7037
- if (existsSync19(filePath)) {
6846
+ const filePath = join21(hooksDir, filename);
6847
+ if (existsSync18(filePath)) {
7038
6848
  unlinkSync7(filePath);
7039
6849
  logs.push(`Removed ${filePath}`);
7040
6850
  }
7041
6851
  }
7042
6852
  if (spec.commandsPath && spec.handoffCommandName) {
7043
6853
  if (spec.commandFormat === "skill") {
7044
- const skillDir = expandPath(join22(spec.commandsPath, spec.handoffCommandName));
7045
- const skillPath = join22(skillDir, "SKILL.md");
7046
- if (existsSync19(skillPath)) {
6854
+ const skillDir = expandPath(join21(spec.commandsPath, spec.handoffCommandName));
6855
+ const skillPath = join21(skillDir, "SKILL.md");
6856
+ if (existsSync18(skillPath)) {
7047
6857
  unlinkSync7(skillPath);
7048
6858
  try {
7049
6859
  rmdirSync(skillDir);
@@ -7052,8 +6862,8 @@ async function uninstallIntegration(agentKey, spec) {
7052
6862
  logs.push(`Removed ${skillPath}`);
7053
6863
  }
7054
6864
  } else {
7055
- const cmdPath = expandPath(join22(spec.commandsPath, `${spec.handoffCommandName}.md`));
7056
- if (existsSync19(cmdPath)) {
6865
+ const cmdPath = expandPath(join21(spec.commandsPath, `${spec.handoffCommandName}.md`));
6866
+ if (existsSync18(cmdPath)) {
7057
6867
  unlinkSync7(cmdPath);
7058
6868
  logs.push(`Removed ${cmdPath}`);
7059
6869
  }
@@ -7071,14 +6881,82 @@ async function uninstallIntegration(agentKey, spec) {
7071
6881
  return { success: false, logs };
7072
6882
  }
7073
6883
  }
6884
+ async function installPluginIntegration(_agentKey, spec) {
6885
+ const logs = [];
6886
+ try {
6887
+ const commandsDir = expandPath(spec.commandsPath);
6888
+ mkdirSync11(commandsDir, { recursive: true });
6889
+ const commandPath = join21(commandsDir, spec.handoffCommandFile);
6890
+ const pluginsDir = expandPath(spec.pluginsPath);
6891
+ mkdirSync11(pluginsDir, { recursive: true });
6892
+ const pluginPath = join21(pluginsDir, spec.pluginFileName);
6893
+ if (existsSync18(commandPath) && existsSync18(pluginPath)) {
6894
+ logs.push("Already installed, skipping.");
6895
+ return { success: true, logs };
6896
+ }
6897
+ if (existsSync18(commandPath) || existsSync18(pluginPath)) {
6898
+ logs.push("Overwriting existing files.");
6899
+ }
6900
+ writeFileSync11(commandPath, generateOpencodeHandoffCommand(spec));
6901
+ logs.push(`Created ${commandPath}`);
6902
+ writeFileSync11(pluginPath, generateOpencodePlugin(spec));
6903
+ logs.push(`Created ${pluginPath}`);
6904
+ return { success: true, logs };
6905
+ } catch (err) {
6906
+ logs.push(`Error: ${err instanceof Error ? err.message : String(err)}`);
6907
+ return { success: false, logs };
6908
+ }
6909
+ }
6910
+ async function uninstallPluginIntegration(_agentKey, spec) {
6911
+ const logs = [];
6912
+ try {
6913
+ const commandPath = join21(expandPath(spec.commandsPath), spec.handoffCommandFile);
6914
+ let removedCount = 0;
6915
+ if (existsSync18(commandPath)) {
6916
+ unlinkSync7(commandPath);
6917
+ logs.push(`Removed ${commandPath}`);
6918
+ removedCount += 1;
6919
+ }
6920
+ const pluginPath = join21(expandPath(spec.pluginsPath), spec.pluginFileName);
6921
+ if (existsSync18(pluginPath)) {
6922
+ unlinkSync7(pluginPath);
6923
+ logs.push(`Removed ${pluginPath}`);
6924
+ removedCount += 1;
6925
+ }
6926
+ if (removedCount === 0) {
6927
+ logs.push("Nothing to remove.");
6928
+ }
6929
+ return { success: true, logs };
6930
+ } catch (err) {
6931
+ logs.push(`Error: ${err instanceof Error ? err.message : String(err)}`);
6932
+ return { success: false, logs };
6933
+ }
6934
+ }
6935
+ async function installIntegration(agentKey, spec) {
6936
+ if (isHooksIntegrationSpec(spec)) {
6937
+ return installHooksIntegration(agentKey, spec);
6938
+ }
6939
+ return installPluginIntegration(agentKey, spec);
6940
+ }
6941
+ async function uninstallIntegration(agentKey, spec) {
6942
+ if (isHooksIntegrationSpec(spec)) {
6943
+ return uninstallHooksIntegration(agentKey, spec);
6944
+ }
6945
+ return uninstallPluginIntegration(agentKey, spec);
6946
+ }
7074
6947
  function buildHandoffItem(agentKey, spec) {
7075
- const hooksDir = expandPath(spec.hooksDirPath);
7076
6948
  return {
7077
6949
  id: "handoff",
7078
6950
  name: "Handoff",
7079
6951
  description: "Transfer sessions between terminal and messaging platforms",
7080
6952
  isInstalled() {
7081
- return existsSync19(join22(hooksDir, "openacp-inject-session.sh")) && existsSync19(join22(hooksDir, "openacp-handoff.sh"));
6953
+ if (isHooksIntegrationSpec(spec)) {
6954
+ const hooksDir = expandPath(spec.hooksDirPath);
6955
+ return existsSync18(join21(hooksDir, "openacp-inject-session.sh")) && existsSync18(join21(hooksDir, "openacp-handoff.sh"));
6956
+ }
6957
+ const commandPath = join21(expandPath(spec.commandsPath), spec.handoffCommandFile);
6958
+ const pluginPath = join21(expandPath(spec.pluginsPath), spec.pluginFileName);
6959
+ return existsSync18(commandPath) && existsSync18(pluginPath);
7082
6960
  },
7083
6961
  install: () => installIntegration(agentKey, spec),
7084
6962
  uninstall: () => uninstallIntegration(agentKey, spec)
@@ -7090,23 +6968,24 @@ function getSkillBasePath(spec) {
7090
6968
  return expandPath(skillsBase);
7091
6969
  }
7092
6970
  function buildTunnelItem(spec) {
7093
- if (!spec.commandsPath) return null;
6971
+ if (!isHooksIntegrationSpec(spec) || !spec.commandsPath) return null;
6972
+ const hooksSpec = spec;
7094
6973
  function getTunnelPath() {
7095
- return join22(getSkillBasePath(spec), "openacp-tunnel", "SKILL.md");
6974
+ return join21(getSkillBasePath(hooksSpec), "openacp-tunnel", "SKILL.md");
7096
6975
  }
7097
6976
  return {
7098
6977
  id: "tunnel",
7099
6978
  name: "Tunnel",
7100
6979
  description: "Expose local ports to the internet via OpenACP tunnel",
7101
6980
  isInstalled() {
7102
- return existsSync19(getTunnelPath());
6981
+ return existsSync18(getTunnelPath());
7103
6982
  },
7104
6983
  async install() {
7105
6984
  const logs = [];
7106
6985
  try {
7107
6986
  const skillPath = getTunnelPath();
7108
- mkdirSync12(dirname12(skillPath), { recursive: true });
7109
- writeFileSync12(skillPath, generateTunnelCommand());
6987
+ mkdirSync11(dirname11(skillPath), { recursive: true });
6988
+ writeFileSync11(skillPath, generateTunnelCommand());
7110
6989
  logs.push(`Created ${skillPath}`);
7111
6990
  return { success: true, logs };
7112
6991
  } catch (err) {
@@ -7118,10 +6997,10 @@ function buildTunnelItem(spec) {
7118
6997
  const logs = [];
7119
6998
  try {
7120
6999
  const skillPath = getTunnelPath();
7121
- if (existsSync19(skillPath)) {
7000
+ if (existsSync18(skillPath)) {
7122
7001
  unlinkSync7(skillPath);
7123
7002
  try {
7124
- rmdirSync(dirname12(skillPath));
7003
+ rmdirSync(dirname11(skillPath));
7125
7004
  } catch {
7126
7005
  }
7127
7006
  logs.push(`Removed ${skillPath}`);
@@ -7557,7 +7436,7 @@ async function buildSettingsKeyboard(core) {
7557
7436
  const fields = getSafeFields();
7558
7437
  const kb = new InlineKeyboard6();
7559
7438
  for (const field of fields) {
7560
- const value = await getFieldValueAsync(field, core.configManager, core.settingsManager);
7439
+ const value = getConfigValue(core.configManager.get(), field.path);
7561
7440
  const label = formatFieldLabel(field, value);
7562
7441
  if (field.type === "toggle") {
7563
7442
  kb.text(`${label}`, `s:toggle:${field.path}`).row();
@@ -7600,11 +7479,10 @@ function setupSettingsCallbacks(bot, core, getAssistantSession) {
7600
7479
  const fieldPath = ctx.callbackQuery.data.replace("s:toggle:", "");
7601
7480
  const fieldDef = getSafeFields().find((f) => f.path === fieldPath);
7602
7481
  if (!fieldDef) return;
7603
- const settingsManager = core.settingsManager;
7604
- const currentValue = await getFieldValueAsync(fieldDef, core.configManager, settingsManager);
7482
+ const currentValue = getConfigValue(core.configManager.get(), fieldDef.path);
7605
7483
  const newValue = !currentValue;
7606
7484
  try {
7607
- await setFieldValueAsync(fieldDef, newValue, core.configManager, settingsManager);
7485
+ await setFieldValueAsync(fieldDef, newValue, core.configManager);
7608
7486
  const toast = isHotReloadable(fieldPath) ? `\u2705 ${fieldPath} = ${newValue}` : `\u2705 ${fieldPath} = ${newValue} (restart needed)`;
7609
7487
  try {
7610
7488
  await ctx.answerCallbackQuery({ text: toast });
@@ -7628,7 +7506,7 @@ function setupSettingsCallbacks(bot, core, getAssistantSession) {
7628
7506
  const fieldDef = getSafeFields().find((f) => f.path === fieldPath);
7629
7507
  if (!fieldDef) return;
7630
7508
  const options = resolveOptions(fieldDef, config) ?? [];
7631
- const currentValue = await getFieldValueAsync(fieldDef, core.configManager, core.settingsManager);
7509
+ const currentValue = getConfigValue(core.configManager.get(), fieldDef.path);
7632
7510
  const kb = new InlineKeyboard6();
7633
7511
  for (const opt of options) {
7634
7512
  const marker = opt === String(currentValue) ? " \u2713" : "";
@@ -7662,9 +7540,7 @@ Select a value:`, {
7662
7540
  const speechSettings = await sm.loadSettings("@openacp/speech");
7663
7541
  hasApiKey = !!speechSettings.groqApiKey;
7664
7542
  } else {
7665
- const config = core.configManager.get();
7666
- const providerConfig = config.speech?.stt?.providers?.[newValue];
7667
- hasApiKey = !!providerConfig?.apiKey;
7543
+ hasApiKey = false;
7668
7544
  }
7669
7545
  if (!hasApiKey) {
7670
7546
  const assistant = getAssistantSession();
@@ -7684,7 +7560,7 @@ Select a value:`, {
7684
7560
  return;
7685
7561
  }
7686
7562
  }
7687
- await setFieldValueAsync(fieldDef, newValue, core.configManager, core.settingsManager);
7563
+ await setFieldValueAsync(fieldDef, newValue, core.configManager);
7688
7564
  try {
7689
7565
  await ctx.answerCallbackQuery({ text: `\u2705 ${fieldPath} = ${newValue}` });
7690
7566
  } catch {
@@ -7709,7 +7585,7 @@ Tap to change:`, {
7709
7585
  const fieldPath = ctx.callbackQuery.data.replace("s:input:", "");
7710
7586
  const fieldDef = getSafeFields().find((f) => f.path === fieldPath);
7711
7587
  if (!fieldDef) return;
7712
- const currentValue = await getFieldValueAsync(fieldDef, core.configManager, core.settingsManager);
7588
+ const currentValue = getConfigValue(core.configManager.get(), fieldDef.path);
7713
7589
  const assistant = getAssistantSession();
7714
7590
  if (!assistant) {
7715
7591
  try {
@@ -8362,7 +8238,7 @@ var init_commands = __esm({
8362
8238
 
8363
8239
  // src/plugins/telegram/permissions.ts
8364
8240
  import { InlineKeyboard as InlineKeyboard11 } from "grammy";
8365
- import { nanoid as nanoid3 } from "nanoid";
8241
+ import { nanoid as nanoid4 } from "nanoid";
8366
8242
  var log30, PermissionHandler;
8367
8243
  var init_permissions = __esm({
8368
8244
  "src/plugins/telegram/permissions.ts"() {
@@ -8381,7 +8257,7 @@ var init_permissions = __esm({
8381
8257
  pending = /* @__PURE__ */ new Map();
8382
8258
  async sendPermissionRequest(session, request) {
8383
8259
  const threadId = Number(session.threadId);
8384
- const callbackKey = nanoid3(8);
8260
+ const callbackKey = nanoid4(8);
8385
8261
  this.pending.set(callbackKey, {
8386
8262
  sessionId: session.id,
8387
8263
  requestId: request.id,
@@ -9270,6 +9146,130 @@ var init_renderer2 = __esm({
9270
9146
  }
9271
9147
  });
9272
9148
 
9149
+ // src/plugins/telegram/validators.ts
9150
+ var validators_exports = {};
9151
+ __export(validators_exports, {
9152
+ checkTopicsPrerequisites: () => checkTopicsPrerequisites,
9153
+ validateBotAdmin: () => validateBotAdmin,
9154
+ validateBotToken: () => validateBotToken,
9155
+ validateChatId: () => validateChatId
9156
+ });
9157
+ async function validateBotToken(token) {
9158
+ try {
9159
+ const res = await fetch(`https://api.telegram.org/bot${token}/getMe`);
9160
+ const data = await res.json();
9161
+ if (data.ok && data.result) {
9162
+ return {
9163
+ ok: true,
9164
+ botName: data.result.first_name,
9165
+ botUsername: data.result.username
9166
+ };
9167
+ }
9168
+ return { ok: false, error: data.description || "Invalid token" };
9169
+ } catch (err) {
9170
+ return { ok: false, error: err.message };
9171
+ }
9172
+ }
9173
+ async function validateChatId(token, chatId) {
9174
+ try {
9175
+ const res = await fetch(`https://api.telegram.org/bot${token}/getChat`, {
9176
+ method: "POST",
9177
+ headers: { "Content-Type": "application/json" },
9178
+ body: JSON.stringify({ chat_id: chatId })
9179
+ });
9180
+ const data = await res.json();
9181
+ if (!data.ok || !data.result) {
9182
+ return { ok: false, error: data.description || "Invalid chat ID" };
9183
+ }
9184
+ if (data.result.type !== "supergroup") {
9185
+ return {
9186
+ ok: false,
9187
+ error: `Chat must be a group (not a channel or private chat). Got: "${data.result.type}"`
9188
+ };
9189
+ }
9190
+ return {
9191
+ ok: true,
9192
+ title: data.result.title,
9193
+ isForum: data.result.is_forum === true
9194
+ };
9195
+ } catch (err) {
9196
+ return { ok: false, error: err.message };
9197
+ }
9198
+ }
9199
+ async function validateBotAdmin(token, chatId) {
9200
+ try {
9201
+ const meRes = await fetch(`https://api.telegram.org/bot${token}/getMe`);
9202
+ const meData = await meRes.json();
9203
+ if (!meData.ok || !meData.result) {
9204
+ return { ok: false, error: "Could not retrieve bot info" };
9205
+ }
9206
+ const res = await fetch(
9207
+ `https://api.telegram.org/bot${token}/getChatMember`,
9208
+ {
9209
+ method: "POST",
9210
+ headers: { "Content-Type": "application/json" },
9211
+ body: JSON.stringify({ chat_id: chatId, user_id: meData.result.id })
9212
+ }
9213
+ );
9214
+ const data = await res.json();
9215
+ if (!data.ok || !data.result) {
9216
+ return {
9217
+ ok: false,
9218
+ error: data.description || "Could not check bot membership"
9219
+ };
9220
+ }
9221
+ const { status } = data.result;
9222
+ if (status === "creator") {
9223
+ return { ok: true, canManageTopics: true };
9224
+ }
9225
+ if (status === "administrator") {
9226
+ return { ok: true, canManageTopics: data.result.can_manage_topics === true };
9227
+ }
9228
+ return {
9229
+ ok: false,
9230
+ error: `Bot is "${status}" in this group. It must be an admin. Please promote the bot to admin in group settings.`
9231
+ };
9232
+ } catch (err) {
9233
+ return { ok: false, error: err.message };
9234
+ }
9235
+ }
9236
+ async function checkTopicsPrerequisites(token, chatId) {
9237
+ const issues = [];
9238
+ try {
9239
+ const res = await fetch(`https://api.telegram.org/bot${token}/getChat`, {
9240
+ method: "POST",
9241
+ headers: { "Content-Type": "application/json" },
9242
+ body: JSON.stringify({ chat_id: chatId })
9243
+ });
9244
+ const data = await res.json();
9245
+ if (data.ok && data.result && !data.result.is_forum) {
9246
+ issues.push(
9247
+ '\u274C Topics are not enabled on this group.\n\u2192 Go to Group Settings \u2192 Edit \u2192 enable "Topics"'
9248
+ );
9249
+ }
9250
+ } catch {
9251
+ issues.push("\u274C Could not check if Topics are enabled (network error).");
9252
+ }
9253
+ const adminResult = await validateBotAdmin(token, chatId);
9254
+ if (!adminResult.ok) {
9255
+ issues.push(
9256
+ `\u274C Bot is not an admin.
9257
+ \u2192 Go to Group Settings \u2192 Administrators \u2192 add the bot \u2192 save`
9258
+ );
9259
+ } else if (!adminResult.canManageTopics) {
9260
+ issues.push(
9261
+ '\u274C Bot cannot manage topics.\n\u2192 In Admin settings, enable the "Manage Topics" permission'
9262
+ );
9263
+ }
9264
+ if (issues.length > 0) return { ok: false, issues };
9265
+ return { ok: true };
9266
+ }
9267
+ var init_validators = __esm({
9268
+ "src/plugins/telegram/validators.ts"() {
9269
+ "use strict";
9270
+ }
9271
+ });
9272
+
9273
9273
  // src/plugins/telegram/adapter.ts
9274
9274
  import { Bot, InputFile } from "grammy";
9275
9275
  function patchedFetch(input2, init) {
@@ -9339,6 +9339,10 @@ var init_adapter = __esm({
9339
9339
  controlMsgIds = /* @__PURE__ */ new Map();
9340
9340
  _threadReadyHandler;
9341
9341
  _configChangedHandler;
9342
+ /** True once topics are initialized and Phase 2 is complete */
9343
+ _topicsInitialized = false;
9344
+ /** Background watcher timer — cancelled on stop() or when topics succeed */
9345
+ _prerequisiteWatcher = null;
9342
9346
  /** Store control message ID in memory + persist to session record */
9343
9347
  storeControlMsgId(sessionId, msgId) {
9344
9348
  this.controlMsgIds.set(sessionId, msgId);
@@ -9467,25 +9471,6 @@ var init_adapter = __esm({
9467
9471
  if (chatId !== this.telegramConfig.chatId) return;
9468
9472
  return next();
9469
9473
  });
9470
- const topics = await this.retryWithBackoff(
9471
- () => ensureTopics(
9472
- this.bot,
9473
- this.telegramConfig.chatId,
9474
- this.telegramConfig,
9475
- async (updates) => {
9476
- if (this.saveTopicIds) {
9477
- await this.saveTopicIds(updates);
9478
- } else {
9479
- await this.core.configManager.save({
9480
- channels: { telegram: updates }
9481
- });
9482
- }
9483
- }
9484
- ),
9485
- "ensureTopics"
9486
- );
9487
- this.notificationTopicId = topics.notificationTopicId;
9488
- this.assistantTopicId = topics.assistantTopicId;
9489
9474
  this.permissionHandler = new PermissionHandler(
9490
9475
  this.bot,
9491
9476
  this.telegramConfig.chatId,
@@ -9495,6 +9480,13 @@ var init_adapter = __esm({
9495
9480
  this.bot.on("message:text", async (ctx, next) => {
9496
9481
  const text3 = ctx.message?.text;
9497
9482
  if (!text3?.startsWith("/")) return next();
9483
+ if (!this._topicsInitialized) {
9484
+ await ctx.reply(
9485
+ "\u23F3 OpenACP is still setting up. Check the General topic for instructions."
9486
+ ).catch(() => {
9487
+ });
9488
+ return;
9489
+ }
9498
9490
  const registry = this.core.lifecycleManager?.serviceRegistry?.get(
9499
9491
  "command-registry"
9500
9492
  );
@@ -9552,6 +9544,11 @@ var init_adapter = __esm({
9552
9544
  }
9553
9545
  });
9554
9546
  this.bot.callbackQuery(/^c\//, async (ctx) => {
9547
+ if (!this._topicsInitialized) {
9548
+ await ctx.answerCallbackQuery().catch(() => {
9549
+ });
9550
+ return;
9551
+ }
9555
9552
  const data = ctx.callbackQuery.data;
9556
9553
  const command = this.fromCallbackData(data);
9557
9554
  const registry = this.core.lifecycleManager?.serviceRegistry?.get(
@@ -9604,10 +9601,84 @@ var init_adapter = __esm({
9604
9601
  await ctx.answerCallbackQuery({ text: "Command failed" });
9605
9602
  }
9606
9603
  });
9607
- setupDangerousModeCallbacks(this.bot, this.core);
9608
- setupTTSCallbacks(this.bot, this.core);
9609
- setupVerbosityCallbacks(this.bot, this.core);
9610
- setupIntegrateCallbacks(this.bot, this.core);
9604
+ setupDangerousModeCallbacks(this.bot, this.core);
9605
+ setupTTSCallbacks(this.bot, this.core);
9606
+ setupVerbosityCallbacks(this.bot, this.core);
9607
+ setupIntegrateCallbacks(this.bot, this.core);
9608
+ this.permissionHandler.setupCallbackHandler();
9609
+ this.bot.start({
9610
+ allowed_updates: ["message", "callback_query"],
9611
+ onStart: () => log33.info({ chatId: this.telegramConfig.chatId }, "Telegram bot started")
9612
+ });
9613
+ const { checkTopicsPrerequisites: checkTopicsPrerequisites2 } = await Promise.resolve().then(() => (init_validators(), validators_exports));
9614
+ const prereqResult = await checkTopicsPrerequisites2(
9615
+ this.telegramConfig.botToken,
9616
+ this.telegramConfig.chatId
9617
+ );
9618
+ if (prereqResult.ok) {
9619
+ await this.initTopicDependentFeatures();
9620
+ } else {
9621
+ for (const issue of prereqResult.issues) {
9622
+ log33.warn({ issue }, "Telegram prerequisite not met");
9623
+ }
9624
+ this.startPrerequisiteWatcher(prereqResult.issues);
9625
+ }
9626
+ }
9627
+ /**
9628
+ * Retry an async operation with exponential backoff.
9629
+ * Used for Telegram API calls that may fail due to transient network issues.
9630
+ */
9631
+ async retryWithBackoff(fn, label, maxRetries = 5, baseDelayMs = 2e3) {
9632
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
9633
+ try {
9634
+ return await fn();
9635
+ } catch (err) {
9636
+ if (attempt === maxRetries) throw err;
9637
+ const delay = baseDelayMs * Math.pow(2, attempt - 1);
9638
+ log33.warn(
9639
+ { err, attempt, maxRetries, delayMs: delay, operation: label },
9640
+ `${label} failed, retrying in ${delay}ms`
9641
+ );
9642
+ await new Promise((r) => setTimeout(r, delay));
9643
+ }
9644
+ }
9645
+ throw new Error("unreachable");
9646
+ }
9647
+ /**
9648
+ * Register Telegram commands in the background with retries.
9649
+ * Non-critical — bot works fine without autocomplete commands.
9650
+ */
9651
+ registerCommandsWithRetry() {
9652
+ this.retryWithBackoff(
9653
+ () => this.bot.api.setMyCommands(STATIC_COMMANDS, {
9654
+ scope: { type: "chat", chat_id: this.telegramConfig.chatId }
9655
+ }),
9656
+ "setMyCommands"
9657
+ ).catch((err) => {
9658
+ log33.warn({ err }, "Failed to register Telegram commands after retries (non-critical)");
9659
+ });
9660
+ }
9661
+ async initTopicDependentFeatures() {
9662
+ if (this._topicsInitialized) return;
9663
+ const topics = await this.retryWithBackoff(
9664
+ () => ensureTopics(
9665
+ this.bot,
9666
+ this.telegramConfig.chatId,
9667
+ this.telegramConfig,
9668
+ async (updates) => {
9669
+ if (this.saveTopicIds) {
9670
+ await this.saveTopicIds(updates);
9671
+ } else {
9672
+ await this.core.configManager.save({
9673
+ channels: { telegram: updates }
9674
+ });
9675
+ }
9676
+ }
9677
+ ),
9678
+ "ensureTopics"
9679
+ );
9680
+ this.notificationTopicId = topics.notificationTopicId;
9681
+ this.assistantTopicId = topics.assistantTopicId;
9611
9682
  setupAllCallbacks(
9612
9683
  this.bot,
9613
9684
  this.core,
@@ -9637,7 +9708,6 @@ ${p}` : p;
9637
9708
  this.storeControlMsgId(sessionId, msgId);
9638
9709
  }
9639
9710
  );
9640
- this.permissionHandler.setupCallbackHandler();
9641
9711
  this._threadReadyHandler = ({ sessionId, channelId, threadId }) => {
9642
9712
  if (channelId !== "telegram") return;
9643
9713
  const session = this.core.sessionManager.getSession(sessionId);
@@ -9675,13 +9745,6 @@ ${p}` : p;
9675
9745
  };
9676
9746
  this.core.eventBus.on("session:configChanged", this._configChangedHandler);
9677
9747
  this.setupRoutes();
9678
- this.bot.start({
9679
- allowed_updates: ["message", "callback_query"],
9680
- onStart: () => log33.info(
9681
- { chatId: this.telegramConfig.chatId },
9682
- "Telegram bot started"
9683
- )
9684
- });
9685
9748
  try {
9686
9749
  const config = this.core.configManager.get();
9687
9750
  const agents = this.core.agentManager.getAvailableAgents();
@@ -9711,42 +9774,56 @@ ${p}` : p;
9711
9774
  } catch (err) {
9712
9775
  log33.error({ err }, "Failed to spawn assistant");
9713
9776
  }
9777
+ this._topicsInitialized = true;
9778
+ log33.info("Telegram adapter fully initialized");
9714
9779
  }
9715
- /**
9716
- * Retry an async operation with exponential backoff.
9717
- * Used for Telegram API calls that may fail due to transient network issues.
9718
- */
9719
- async retryWithBackoff(fn, label, maxRetries = 5, baseDelayMs = 2e3) {
9720
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
9721
- try {
9722
- return await fn();
9723
- } catch (err) {
9724
- if (attempt === maxRetries) throw err;
9725
- const delay = baseDelayMs * Math.pow(2, attempt - 1);
9726
- log33.warn(
9727
- { err, attempt, maxRetries, delayMs: delay, operation: label },
9728
- `${label} failed, retrying in ${delay}ms`
9729
- );
9730
- await new Promise((r) => setTimeout(r, delay));
9731
- }
9732
- }
9733
- throw new Error("unreachable");
9734
- }
9735
- /**
9736
- * Register Telegram commands in the background with retries.
9737
- * Non-critical — bot works fine without autocomplete commands.
9738
- */
9739
- registerCommandsWithRetry() {
9740
- this.retryWithBackoff(
9741
- () => this.bot.api.setMyCommands(STATIC_COMMANDS, {
9742
- scope: { type: "chat", chat_id: this.telegramConfig.chatId }
9743
- }),
9744
- "setMyCommands"
9745
- ).catch((err) => {
9746
- log33.warn({ err }, "Failed to register Telegram commands after retries (non-critical)");
9780
+ startPrerequisiteWatcher(issues) {
9781
+ const setupMessage = `\u26A0\uFE0F <b>OpenACP needs setup before it can start.</b>
9782
+
9783
+ ` + issues.join("\n\n") + `
9784
+
9785
+ OpenACP will automatically retry until this is resolved.`;
9786
+ this.bot.api.sendMessage(this.telegramConfig.chatId, setupMessage, {
9787
+ parse_mode: "HTML"
9788
+ }).catch((err) => {
9789
+ log33.warn({ err }, "Failed to send setup guidance to General topic");
9747
9790
  });
9791
+ const schedule = [5e3, 1e4, 3e4];
9792
+ let attempt = 1;
9793
+ const retry = async () => {
9794
+ if (this._prerequisiteWatcher === null) return;
9795
+ const { checkTopicsPrerequisites: checkTopicsPrerequisites2 } = await Promise.resolve().then(() => (init_validators(), validators_exports));
9796
+ const result = await checkTopicsPrerequisites2(
9797
+ this.telegramConfig.botToken,
9798
+ this.telegramConfig.chatId
9799
+ );
9800
+ if (result.ok) {
9801
+ this._prerequisiteWatcher = null;
9802
+ log33.info("Prerequisites met \u2014 completing Telegram adapter initialization");
9803
+ try {
9804
+ await this.initTopicDependentFeatures();
9805
+ await this.bot.api.sendMessage(
9806
+ this.telegramConfig.chatId,
9807
+ "\u2705 <b>OpenACP is ready!</b>\n\nSystem topics have been created. Use the \u{1F916} Assistant topic to get started.",
9808
+ { parse_mode: "HTML" }
9809
+ );
9810
+ } catch (err) {
9811
+ log33.error({ err }, "Failed to complete initialization after prerequisites met");
9812
+ }
9813
+ return;
9814
+ }
9815
+ log33.debug({ issues: result.issues }, "Prerequisites not yet met, retrying");
9816
+ const delay = schedule[Math.min(attempt, schedule.length - 1)];
9817
+ attempt++;
9818
+ this._prerequisiteWatcher = setTimeout(retry, delay);
9819
+ };
9820
+ this._prerequisiteWatcher = setTimeout(retry, schedule[0]);
9748
9821
  }
9749
9822
  async stop() {
9823
+ if (this._prerequisiteWatcher !== null) {
9824
+ clearTimeout(this._prerequisiteWatcher);
9825
+ this._prerequisiteWatcher = null;
9826
+ }
9750
9827
  for (const tracker of this.sessionTrackers.values()) {
9751
9828
  tracker.destroy();
9752
9829
  }
@@ -10532,8 +10609,16 @@ function nodeToWebWritable(nodeStream) {
10532
10609
  resolve6();
10533
10610
  return;
10534
10611
  }
10535
- nodeStream.once("drain", resolve6);
10536
- nodeStream.once("error", reject);
10612
+ const onDrain = () => {
10613
+ nodeStream.removeListener("error", onError);
10614
+ resolve6();
10615
+ };
10616
+ const onError = (err) => {
10617
+ nodeStream.removeListener("drain", onDrain);
10618
+ reject(err);
10619
+ };
10620
+ nodeStream.once("drain", onDrain);
10621
+ nodeStream.once("error", onError);
10537
10622
  });
10538
10623
  },
10539
10624
  close() {
@@ -10580,13 +10665,13 @@ init_config();
10580
10665
  // src/core/agents/agent-instance.ts
10581
10666
  import { spawn as spawn2, execFileSync } from "child_process";
10582
10667
  import { Transform } from "stream";
10583
- import fs9 from "fs";
10584
- import path8 from "path";
10668
+ import fs8 from "fs";
10669
+ import path7 from "path";
10585
10670
  import { ClientSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
10586
10671
 
10587
10672
  // src/core/security/path-guard.ts
10588
- import fs6 from "fs";
10589
- import path6 from "path";
10673
+ import fs5 from "fs";
10674
+ import path5 from "path";
10590
10675
  import ignore from "ignore";
10591
10676
  var DEFAULT_DENY_PATTERNS = [
10592
10677
  ".env",
@@ -10606,15 +10691,15 @@ var PathGuard = class {
10606
10691
  ig;
10607
10692
  constructor(options) {
10608
10693
  try {
10609
- this.cwd = fs6.realpathSync(path6.resolve(options.cwd));
10694
+ this.cwd = fs5.realpathSync(path5.resolve(options.cwd));
10610
10695
  } catch {
10611
- this.cwd = path6.resolve(options.cwd);
10696
+ this.cwd = path5.resolve(options.cwd);
10612
10697
  }
10613
10698
  this.allowedPaths = options.allowedPaths.map((p) => {
10614
10699
  try {
10615
- return fs6.realpathSync(path6.resolve(p));
10700
+ return fs5.realpathSync(path5.resolve(p));
10616
10701
  } catch {
10617
- return path6.resolve(p);
10702
+ return path5.resolve(p);
10618
10703
  }
10619
10704
  });
10620
10705
  this.ig = ignore();
@@ -10624,19 +10709,19 @@ var PathGuard = class {
10624
10709
  }
10625
10710
  }
10626
10711
  validatePath(targetPath, operation) {
10627
- const resolved = path6.resolve(targetPath);
10712
+ const resolved = path5.resolve(targetPath);
10628
10713
  let realPath;
10629
10714
  try {
10630
- realPath = fs6.realpathSync(resolved);
10715
+ realPath = fs5.realpathSync(resolved);
10631
10716
  } catch {
10632
10717
  realPath = resolved;
10633
10718
  }
10634
- if (operation === "write" && path6.basename(realPath) === ".openacpignore") {
10719
+ if (operation === "write" && path5.basename(realPath) === ".openacpignore") {
10635
10720
  return { allowed: false, reason: "Cannot write to .openacpignore" };
10636
10721
  }
10637
- const isWithinCwd = realPath === this.cwd || realPath.startsWith(this.cwd + path6.sep);
10722
+ const isWithinCwd = realPath === this.cwd || realPath.startsWith(this.cwd + path5.sep);
10638
10723
  const isWithinAllowed = this.allowedPaths.some(
10639
- (ap) => realPath === ap || realPath.startsWith(ap + path6.sep)
10724
+ (ap) => realPath === ap || realPath.startsWith(ap + path5.sep)
10640
10725
  );
10641
10726
  if (!isWithinCwd && !isWithinAllowed) {
10642
10727
  return {
@@ -10645,7 +10730,7 @@ var PathGuard = class {
10645
10730
  };
10646
10731
  }
10647
10732
  if (isWithinCwd && !isWithinAllowed) {
10648
- const relativePath = path6.relative(this.cwd, realPath);
10733
+ const relativePath = path5.relative(this.cwd, realPath);
10649
10734
  if (relativePath === ".openacpignore") {
10650
10735
  return { allowed: true, reason: "" };
10651
10736
  }
@@ -10660,15 +10745,15 @@ var PathGuard = class {
10660
10745
  }
10661
10746
  addAllowedPath(p) {
10662
10747
  try {
10663
- this.allowedPaths.push(fs6.realpathSync(path6.resolve(p)));
10748
+ this.allowedPaths.push(fs5.realpathSync(path5.resolve(p)));
10664
10749
  } catch {
10665
- this.allowedPaths.push(path6.resolve(p));
10750
+ this.allowedPaths.push(path5.resolve(p));
10666
10751
  }
10667
10752
  }
10668
10753
  static loadIgnoreFile(cwd) {
10669
- const ignorePath = path6.join(cwd, ".openacpignore");
10754
+ const ignorePath = path5.join(cwd, ".openacpignore");
10670
10755
  try {
10671
- const content = fs6.readFileSync(ignorePath, "utf-8");
10756
+ const content = fs5.readFileSync(ignorePath, "utf-8");
10672
10757
  return content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
10673
10758
  } catch {
10674
10759
  return [];
@@ -10723,7 +10808,8 @@ function filterEnv(processEnv, agentEnv, whitelist) {
10723
10808
  }
10724
10809
 
10725
10810
  // src/core/utils/typed-emitter.ts
10726
- var TypedEmitter = class {
10811
+ var TypedEmitter = class _TypedEmitter {
10812
+ static MAX_BUFFER_SIZE = 1e4;
10727
10813
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
10728
10814
  listeners = /* @__PURE__ */ new Map();
10729
10815
  paused = false;
@@ -10747,6 +10833,10 @@ var TypedEmitter = class {
10747
10833
  this.deliver(event, args);
10748
10834
  } else {
10749
10835
  this.buffer.push({ event, args });
10836
+ if (this.buffer.length > _TypedEmitter.MAX_BUFFER_SIZE) {
10837
+ console.warn(`[TypedEmitter] Buffer exceeded ${_TypedEmitter.MAX_BUFFER_SIZE} events, dropping oldest`);
10838
+ this.buffer.shift();
10839
+ }
10750
10840
  }
10751
10841
  return;
10752
10842
  }
@@ -10791,7 +10881,11 @@ var TypedEmitter = class {
10791
10881
  const set = this.listeners.get(event);
10792
10882
  if (!set) return;
10793
10883
  for (const listener of set) {
10794
- listener(...args);
10884
+ try {
10885
+ listener(...args);
10886
+ } catch (err) {
10887
+ console.error(`[EventBus] Listener error on "${String(event)}":`, err);
10888
+ }
10795
10889
  }
10796
10890
  }
10797
10891
  };
@@ -10882,6 +10976,11 @@ var TerminalManager = class {
10882
10976
  }, async (p) => p).catch(() => {
10883
10977
  });
10884
10978
  }
10979
+ setTimeout(() => {
10980
+ if (this.terminals.has(terminalId)) {
10981
+ this.terminals.delete(terminalId);
10982
+ }
10983
+ }, 3e4).unref();
10885
10984
  });
10886
10985
  return { terminalId };
10887
10986
  }
@@ -10914,6 +11013,12 @@ var TerminalManager = class {
10914
11013
  state.process.on("exit", (code, signal) => {
10915
11014
  resolve6({ exitCode: code, signal });
10916
11015
  });
11016
+ if (state.exitStatus !== null) {
11017
+ resolve6({
11018
+ exitCode: state.exitStatus.exitCode,
11019
+ signal: state.exitStatus.signal
11020
+ });
11021
+ }
10917
11022
  });
10918
11023
  }
10919
11024
  kill(terminalId) {
@@ -10951,24 +11056,24 @@ var McpManager = class {
10951
11056
  };
10952
11057
 
10953
11058
  // src/core/utils/debug-tracer.ts
10954
- import fs8 from "fs";
10955
- import path7 from "path";
11059
+ import fs7 from "fs";
11060
+ import path6 from "path";
10956
11061
  var DEBUG_ENABLED = process.env.OPENACP_DEBUG === "true" || process.env.OPENACP_DEBUG === "1";
10957
11062
  var DebugTracer = class {
10958
11063
  constructor(sessionId, workingDirectory) {
10959
11064
  this.sessionId = sessionId;
10960
11065
  this.workingDirectory = workingDirectory;
10961
- this.logDir = path7.join(workingDirectory, ".log");
11066
+ this.logDir = path6.join(workingDirectory, ".log");
10962
11067
  }
10963
11068
  dirCreated = false;
10964
11069
  logDir;
10965
11070
  log(layer, data) {
10966
11071
  try {
10967
11072
  if (!this.dirCreated) {
10968
- fs8.mkdirSync(this.logDir, { recursive: true });
11073
+ fs7.mkdirSync(this.logDir, { recursive: true });
10969
11074
  this.dirCreated = true;
10970
11075
  }
10971
- const filePath = path7.join(this.logDir, `${this.sessionId}_${layer}.jsonl`);
11076
+ const filePath = path6.join(this.logDir, `${this.sessionId}_${layer}.jsonl`);
10972
11077
  const seen = /* @__PURE__ */ new WeakSet();
10973
11078
  const line = JSON.stringify({ ts: Date.now(), ...data }, (_key, value) => {
10974
11079
  if (typeof value === "object" && value !== null) {
@@ -10977,7 +11082,7 @@ var DebugTracer = class {
10977
11082
  }
10978
11083
  return value;
10979
11084
  }) + "\n";
10980
- fs8.appendFileSync(filePath, line);
11085
+ fs7.appendFileSync(filePath, line);
10981
11086
  } catch {
10982
11087
  }
10983
11088
  }
@@ -10995,11 +11100,11 @@ init_log();
10995
11100
  var log4 = createChildLogger({ module: "agent-instance" });
10996
11101
  function findPackageRoot(startDir) {
10997
11102
  let dir = startDir;
10998
- while (dir !== path8.dirname(dir)) {
10999
- if (fs9.existsSync(path8.join(dir, "package.json"))) {
11103
+ while (dir !== path7.dirname(dir)) {
11104
+ if (fs8.existsSync(path7.join(dir, "package.json"))) {
11000
11105
  return dir;
11001
11106
  }
11002
- dir = path8.dirname(dir);
11107
+ dir = path7.dirname(dir);
11003
11108
  }
11004
11109
  return startDir;
11005
11110
  }
@@ -11011,26 +11116,26 @@ function resolveAgentCommand(cmd) {
11011
11116
  }
11012
11117
  for (const root of searchRoots) {
11013
11118
  const packageDirs = [
11014
- path8.resolve(root, "node_modules", "@zed-industries", cmd, "dist", "index.js"),
11015
- path8.resolve(root, "node_modules", cmd, "dist", "index.js")
11119
+ path7.resolve(root, "node_modules", "@zed-industries", cmd, "dist", "index.js"),
11120
+ path7.resolve(root, "node_modules", cmd, "dist", "index.js")
11016
11121
  ];
11017
11122
  for (const jsPath of packageDirs) {
11018
- if (fs9.existsSync(jsPath)) {
11123
+ if (fs8.existsSync(jsPath)) {
11019
11124
  return { command: process.execPath, args: [jsPath] };
11020
11125
  }
11021
11126
  }
11022
11127
  }
11023
11128
  for (const root of searchRoots) {
11024
- const localBin = path8.resolve(root, "node_modules", ".bin", cmd);
11025
- if (fs9.existsSync(localBin)) {
11026
- const content = fs9.readFileSync(localBin, "utf-8");
11129
+ const localBin = path7.resolve(root, "node_modules", ".bin", cmd);
11130
+ if (fs8.existsSync(localBin)) {
11131
+ const content = fs8.readFileSync(localBin, "utf-8");
11027
11132
  if (content.startsWith("#!/usr/bin/env node")) {
11028
11133
  return { command: process.execPath, args: [localBin] };
11029
11134
  }
11030
11135
  const match = content.match(/"([^"]+\.js)"/);
11031
11136
  if (match) {
11032
- const target = path8.resolve(path8.dirname(localBin), match[1]);
11033
- if (fs9.existsSync(target)) {
11137
+ const target = path7.resolve(path7.dirname(localBin), match[1]);
11138
+ if (fs8.existsSync(target)) {
11034
11139
  return { command: process.execPath, args: [target] };
11035
11140
  }
11036
11141
  }
@@ -11039,7 +11144,7 @@ function resolveAgentCommand(cmd) {
11039
11144
  try {
11040
11145
  const fullPath = execFileSync("which", [cmd], { encoding: "utf-8" }).trim();
11041
11146
  if (fullPath) {
11042
- const content = fs9.readFileSync(fullPath, "utf-8");
11147
+ const content = fs8.readFileSync(fullPath, "utf-8");
11043
11148
  if (content.startsWith("#!/usr/bin/env node")) {
11044
11149
  return { command: process.execPath, args: [fullPath] };
11045
11150
  }
@@ -11471,8 +11576,8 @@ ${stderr}`
11471
11576
  writePath = result.path;
11472
11577
  writeContent = result.content;
11473
11578
  }
11474
- await fs9.promises.mkdir(path8.dirname(writePath), { recursive: true });
11475
- await fs9.promises.writeFile(writePath, writeContent, "utf-8");
11579
+ await fs8.promises.mkdir(path7.dirname(writePath), { recursive: true });
11580
+ await fs8.promises.writeFile(writePath, writeContent, "utf-8");
11476
11581
  return {};
11477
11582
  },
11478
11583
  // ── Terminal operations (delegated to TerminalManager) ─────────────
@@ -11574,7 +11679,7 @@ ${stderr}`
11574
11679
  [Attachment access denied: ${attCheck.reason}]`;
11575
11680
  continue;
11576
11681
  }
11577
- const data = await fs9.promises.readFile(att.filePath);
11682
+ const data = await fs8.promises.readFile(att.filePath);
11578
11683
  contentBlocks.push({ type: "image", data: data.toString("base64"), mimeType: att.mimeType });
11579
11684
  } else if (att.type === "audio" && this.promptCapabilities?.audio && !tooLarge) {
11580
11685
  const attCheck = this.pathGuard.validatePath(att.filePath, "read");
@@ -11584,7 +11689,7 @@ ${stderr}`
11584
11689
  [Attachment access denied: ${attCheck.reason}]`;
11585
11690
  continue;
11586
11691
  }
11587
- const data = await fs9.promises.readFile(att.filePath);
11692
+ const data = await fs8.promises.readFile(att.filePath);
11588
11693
  contentBlocks.push({ type: "audio", data: data.toString("base64"), mimeType: att.mimeType });
11589
11694
  } else {
11590
11695
  if ((att.type === "image" || att.type === "audio") && !tooLarge) {
@@ -11669,21 +11774,27 @@ var PromptQueue = class {
11669
11774
  queue = [];
11670
11775
  processing = false;
11671
11776
  abortController = null;
11672
- async enqueue(text3, attachments, routing) {
11777
+ /** Set when abort is triggered; drainNext waits for the current processor to settle before starting the next item. */
11778
+ processorSettled = null;
11779
+ async enqueue(text3, attachments, routing, turnId) {
11673
11780
  if (this.processing) {
11674
11781
  return new Promise((resolve6) => {
11675
- this.queue.push({ text: text3, attachments, routing, resolve: resolve6 });
11782
+ this.queue.push({ text: text3, attachments, routing, turnId, resolve: resolve6 });
11676
11783
  });
11677
11784
  }
11678
- await this.process(text3, attachments, routing);
11785
+ await this.process(text3, attachments, routing, turnId);
11679
11786
  }
11680
- async process(text3, attachments, routing) {
11787
+ async process(text3, attachments, routing, turnId) {
11681
11788
  this.processing = true;
11682
11789
  this.abortController = new AbortController();
11683
11790
  const { signal } = this.abortController;
11791
+ let settledResolve;
11792
+ this.processorSettled = new Promise((r) => {
11793
+ settledResolve = r;
11794
+ });
11684
11795
  try {
11685
11796
  await Promise.race([
11686
- this.processor(text3, attachments, routing),
11797
+ this.processor(text3, attachments, routing, turnId),
11687
11798
  new Promise((_, reject) => {
11688
11799
  signal.addEventListener("abort", () => reject(new Error("Prompt aborted")), { once: true });
11689
11800
  })
@@ -11695,13 +11806,15 @@ var PromptQueue = class {
11695
11806
  } finally {
11696
11807
  this.abortController = null;
11697
11808
  this.processing = false;
11809
+ settledResolve();
11810
+ this.processorSettled = null;
11698
11811
  this.drainNext();
11699
11812
  }
11700
11813
  }
11701
11814
  drainNext() {
11702
11815
  const next = this.queue.shift();
11703
11816
  if (next) {
11704
- this.process(next.text, next.attachments, next.routing).then(next.resolve);
11817
+ this.process(next.text, next.attachments, next.routing, next.turnId).then(next.resolve);
11705
11818
  }
11706
11819
  }
11707
11820
  clear() {
@@ -11790,13 +11903,13 @@ var PermissionGate = class {
11790
11903
 
11791
11904
  // src/core/sessions/session.ts
11792
11905
  init_log();
11793
- import * as fs10 from "fs";
11906
+ import * as fs9 from "fs";
11794
11907
 
11795
11908
  // src/core/sessions/turn-context.ts
11796
11909
  import { nanoid } from "nanoid";
11797
- function createTurnContext(sourceAdapterId, responseAdapterId) {
11910
+ function createTurnContext(sourceAdapterId, responseAdapterId, turnId) {
11798
11911
  return {
11799
- turnId: nanoid(8),
11912
+ turnId: turnId ?? nanoid(8),
11800
11913
  sourceAdapterId,
11801
11914
  responseAdapterId
11802
11915
  };
@@ -11870,6 +11983,9 @@ var Session = class extends TypedEmitter {
11870
11983
  threadIds = /* @__PURE__ */ new Map();
11871
11984
  /** Active turn context — sealed on prompt dequeue, cleared on turn end */
11872
11985
  activeTurnContext = null;
11986
+ /** The agentInstance for which the agent→session event relay is wired (prevents duplicate relays from multiple bridges).
11987
+ * When the agent is swapped, the relay must be re-wired to the new instance. */
11988
+ agentRelaySource = null;
11873
11989
  permissionGate = new PermissionGate();
11874
11990
  queue;
11875
11991
  speechService;
@@ -11888,7 +12004,7 @@ var Session = class extends TypedEmitter {
11888
12004
  this.log = createSessionLogger(this.id, moduleLog);
11889
12005
  this.log.info({ agentName: this.agentName }, "Session created");
11890
12006
  this.queue = new PromptQueue(
11891
- (text3, attachments, routing) => this.processPrompt(text3, attachments, routing),
12007
+ (text3, attachments, routing, turnId) => this.processPrompt(text3, attachments, routing, turnId),
11892
12008
  (err) => {
11893
12009
  this.log.error({ err }, "Prompt execution failed");
11894
12010
  const message = err instanceof Error ? err.message : String(err);
@@ -11963,22 +12079,26 @@ var Session = class extends TypedEmitter {
11963
12079
  this.log.info({ voiceMode: mode }, "TTS mode changed");
11964
12080
  }
11965
12081
  // --- Public API ---
11966
- async enqueuePrompt(text3, attachments, routing) {
12082
+ async enqueuePrompt(text3, attachments, routing, externalTurnId) {
12083
+ const turnId = externalTurnId ?? nanoid2(8);
11967
12084
  if (this.middlewareChain) {
11968
12085
  const payload = { text: text3, attachments, sessionId: this.id };
11969
12086
  const result = await this.middlewareChain.execute("agent:beforePrompt", payload, async (p) => p);
11970
- if (!result) return;
12087
+ if (!result) return turnId;
11971
12088
  text3 = result.text;
11972
12089
  attachments = result.attachments;
11973
12090
  }
11974
- await this.queue.enqueue(text3, attachments, routing);
12091
+ await this.queue.enqueue(text3, attachments, routing, turnId);
12092
+ return turnId;
11975
12093
  }
11976
- async processPrompt(text3, attachments, routing) {
12094
+ async processPrompt(text3, attachments, routing, turnId) {
11977
12095
  if (this._status === "finished") return;
11978
12096
  this.activeTurnContext = createTurnContext(
11979
12097
  routing?.sourceAdapterId ?? this.channelId,
11980
- routing?.responseAdapterId
12098
+ routing?.responseAdapterId,
12099
+ turnId
11981
12100
  );
12101
+ this.emit("turn_started", this.activeTurnContext);
11982
12102
  this.promptCount++;
11983
12103
  this.emit("prompt_count_changed", this.promptCount);
11984
12104
  if (this._status === "initializing" || this._status === "cancelled" || this._status === "error") {
@@ -12016,6 +12136,7 @@ ${text3}`;
12016
12136
  });
12017
12137
  }
12018
12138
  let stopReason = "end_turn";
12139
+ let promptError;
12019
12140
  try {
12020
12141
  const response = await this.agentInstance.prompt(processed.text, processed.attachments);
12021
12142
  if (response && typeof response === "object" && "stopReason" in response) {
@@ -12027,14 +12148,21 @@ ${text3}`;
12027
12148
  if (ttsActive && this.voiceMode === "next") {
12028
12149
  this.voiceMode = "off";
12029
12150
  }
12151
+ } catch (err) {
12152
+ stopReason = "error";
12153
+ promptError = err;
12030
12154
  } finally {
12031
12155
  if (accumulatorListener) {
12032
12156
  this.off("agent_event", accumulatorListener);
12033
12157
  }
12158
+ if (this.middlewareChain) {
12159
+ this.middlewareChain.execute("turn:end", { sessionId: this.id, stopReason, durationMs: Date.now() - promptStart }, async (p) => p).catch(() => {
12160
+ });
12161
+ }
12162
+ this.activeTurnContext = null;
12034
12163
  }
12035
- if (this.middlewareChain) {
12036
- this.middlewareChain.execute("turn:end", { sessionId: this.id, stopReason, durationMs: Date.now() - promptStart }, async (p) => p).catch(() => {
12037
- });
12164
+ if (promptError !== void 0) {
12165
+ throw promptError;
12038
12166
  }
12039
12167
  this.log.info(
12040
12168
  { durationMs: Date.now() - promptStart },
@@ -12045,7 +12173,6 @@ ${text3}`;
12045
12173
  this.log.warn({ err }, "TTS post-processing failed");
12046
12174
  });
12047
12175
  }
12048
- this.activeTurnContext = null;
12049
12176
  if (!this.name) {
12050
12177
  await this.autoName();
12051
12178
  }
@@ -12071,7 +12198,7 @@ ${text3}`;
12071
12198
  try {
12072
12199
  const audioPath = att.originalFilePath || att.filePath;
12073
12200
  const audioMime = att.originalFilePath ? "audio/ogg" : att.mimeType;
12074
- const audioBuffer = await fs10.promises.readFile(audioPath);
12201
+ const audioBuffer = await fs9.promises.readFile(audioPath);
12075
12202
  const result = await this.speechService.transcribe(audioBuffer, audioMime);
12076
12203
  this.log.info({ provider: "stt", duration: result.duration }, "Voice transcribed");
12077
12204
  this.emit("agent_event", {
@@ -12303,7 +12430,7 @@ ${result.text}` : result.text;
12303
12430
  };
12304
12431
 
12305
12432
  // src/core/message-transformer.ts
12306
- import * as path9 from "path";
12433
+ import * as path8 from "path";
12307
12434
 
12308
12435
  // src/core/utils/extract-file-info.ts
12309
12436
  function extractFileInfo(name, kind, content, rawInput, meta) {
@@ -12613,6 +12740,20 @@ var MessageTransformer = class {
12613
12740
  return { type: "text", text: "" };
12614
12741
  }
12615
12742
  }
12743
+ /** Clear cached entries whose key starts with the given prefix. */
12744
+ clearSessionCaches(prefix) {
12745
+ for (const key of this.toolRawInputCache.keys()) {
12746
+ if (key.startsWith(prefix)) this.toolRawInputCache.delete(key);
12747
+ }
12748
+ for (const key of this.toolViewerCache.keys()) {
12749
+ if (key.startsWith(prefix)) this.toolViewerCache.delete(key);
12750
+ }
12751
+ }
12752
+ /** Clear all caches. */
12753
+ clearCaches() {
12754
+ this.toolRawInputCache.clear();
12755
+ this.toolViewerCache.clear();
12756
+ }
12616
12757
  /** Check if rawInput is a non-empty object (not null, not {}) */
12617
12758
  isNonEmptyInput(input2) {
12618
12759
  return input2 !== null && input2 !== void 0 && typeof input2 === "object" && !Array.isArray(input2) && Object.keys(input2).length > 0;
@@ -12661,7 +12802,7 @@ var MessageTransformer = class {
12661
12802
  );
12662
12803
  return;
12663
12804
  }
12664
- const fileExt = path9.extname(fileInfo.filePath).toLowerCase();
12805
+ const fileExt = path8.extname(fileInfo.filePath).toLowerCase();
12665
12806
  if (BINARY_VIEWER_EXTENSIONS.has(fileExt)) {
12666
12807
  log5.debug({ kind, filePath: fileInfo.filePath }, "enrichWithViewerLinks: skipping binary file");
12667
12808
  return;
@@ -12807,6 +12948,7 @@ var SessionManager = class {
12807
12948
  } catch {
12808
12949
  }
12809
12950
  session.markCancelled();
12951
+ await session.destroy();
12810
12952
  this.sessions.delete(sessionId);
12811
12953
  }
12812
12954
  if (this.store) {
@@ -13008,9 +13150,12 @@ var SessionBridge = class {
13008
13150
  connect() {
13009
13151
  if (this.connected) return;
13010
13152
  this.connected = true;
13011
- this.listen(this.session.agentInstance, "agent_event", (event) => {
13012
- this.session.emit("agent_event", event);
13013
- });
13153
+ if (this.session.agentRelaySource !== this.session.agentInstance) {
13154
+ this.listen(this.session.agentInstance, "agent_event", (event) => {
13155
+ this.session.emit("agent_event", event);
13156
+ });
13157
+ this.session.agentRelaySource = this.session.agentInstance;
13158
+ }
13014
13159
  this.listen(this.session, "agent_event", (event) => {
13015
13160
  if (this.shouldForward(event)) {
13016
13161
  this.dispatchAgentEvent(event);
@@ -13063,6 +13208,16 @@ var SessionBridge = class {
13063
13208
  this.listen(this.session, "prompt_count_changed", (count) => {
13064
13209
  this.deps.sessionManager.patchRecord(this.session.id, { currentPromptCount: count });
13065
13210
  });
13211
+ this.listen(this.session, "turn_started", (ctx) => {
13212
+ if (ctx.sourceAdapterId !== "sse" && ctx.sourceAdapterId !== "api") {
13213
+ this.deps.eventBus?.emit("message:processing", {
13214
+ sessionId: this.session.id,
13215
+ turnId: ctx.turnId,
13216
+ sourceAdapterId: ctx.sourceAdapterId,
13217
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
13218
+ });
13219
+ }
13220
+ });
13066
13221
  if (this.session.latestCommands !== null) {
13067
13222
  this.session.emit("agent_event", { type: "commands_update", commands: this.session.latestCommands });
13068
13223
  }
@@ -13079,6 +13234,7 @@ var SessionBridge = class {
13079
13234
  if (current?.__bridgeId === this.adapterId) {
13080
13235
  this.session.agentInstance.onPermissionRequest = async () => "";
13081
13236
  }
13237
+ this.deps.messageTransformer.clearSessionCaches?.(this.session.id);
13082
13238
  }
13083
13239
  /** Dispatch an agent event through middleware and to the adapter */
13084
13240
  async dispatchAgentEvent(event) {
@@ -13161,12 +13317,12 @@ var SessionBridge = class {
13161
13317
  break;
13162
13318
  case "image_content": {
13163
13319
  if (this.deps.fileService) {
13164
- const fs34 = this.deps.fileService;
13320
+ const fs33 = this.deps.fileService;
13165
13321
  const sid = this.session.id;
13166
13322
  const { data, mimeType } = event;
13167
13323
  const buffer = Buffer.from(data, "base64");
13168
- const ext = fs34.extensionFromMime(mimeType);
13169
- fs34.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
13324
+ const ext = fs33.extensionFromMime(mimeType);
13325
+ fs33.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
13170
13326
  this.sendMessage(sid, {
13171
13327
  type: "attachment",
13172
13328
  text: "",
@@ -13178,12 +13334,12 @@ var SessionBridge = class {
13178
13334
  }
13179
13335
  case "audio_content": {
13180
13336
  if (this.deps.fileService) {
13181
- const fs34 = this.deps.fileService;
13337
+ const fs33 = this.deps.fileService;
13182
13338
  const sid = this.session.id;
13183
13339
  const { data, mimeType } = event;
13184
13340
  const buffer = Buffer.from(data, "base64");
13185
- const ext = fs34.extensionFromMime(mimeType);
13186
- fs34.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
13341
+ const ext = fs33.extensionFromMime(mimeType);
13342
+ fs33.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
13187
13343
  this.sendMessage(sid, {
13188
13344
  type: "attachment",
13189
13345
  text: "",
@@ -13369,14 +13525,29 @@ var SessionFactory = class {
13369
13525
  }
13370
13526
  let agentInstance;
13371
13527
  try {
13372
- agentInstance = createParams.resumeAgentSessionId ? await this.agentManager.resume(
13373
- createParams.agentName,
13374
- createParams.workingDirectory,
13375
- createParams.resumeAgentSessionId
13376
- ) : await this.agentManager.spawn(
13377
- createParams.agentName,
13378
- createParams.workingDirectory
13379
- );
13528
+ if (createParams.resumeAgentSessionId) {
13529
+ try {
13530
+ agentInstance = await this.agentManager.resume(
13531
+ createParams.agentName,
13532
+ createParams.workingDirectory,
13533
+ createParams.resumeAgentSessionId
13534
+ );
13535
+ } catch (resumeErr) {
13536
+ log7.warn(
13537
+ { agentName: createParams.agentName, resumeErr },
13538
+ "Agent session resume failed, falling back to fresh spawn"
13539
+ );
13540
+ agentInstance = await this.agentManager.spawn(
13541
+ createParams.agentName,
13542
+ createParams.workingDirectory
13543
+ );
13544
+ }
13545
+ } else {
13546
+ agentInstance = await this.agentManager.spawn(
13547
+ createParams.agentName,
13548
+ createParams.workingDirectory
13549
+ );
13550
+ }
13380
13551
  } catch (err) {
13381
13552
  const message = err instanceof Error ? err.message : String(err);
13382
13553
  const guidanceLines = [
@@ -13405,31 +13576,9 @@ var SessionFactory = class {
13405
13576
  type: "system_message",
13406
13577
  message: guidanceLines.join("\n")
13407
13578
  };
13408
- const failedSession = new Session({
13409
- id: createParams.existingSessionId,
13410
- channelId: createParams.channelId,
13411
- agentName: createParams.agentName,
13412
- workingDirectory: createParams.workingDirectory,
13413
- // Dummy agent instance — will never be prompted
13414
- agentInstance: {
13415
- sessionId: "",
13416
- prompt: async () => {
13417
- },
13418
- cancel: async () => {
13419
- },
13420
- destroy: async () => {
13421
- },
13422
- on: () => {
13423
- },
13424
- off: () => {
13425
- }
13426
- },
13427
- speechService: this.speechService
13428
- });
13429
- this.sessionManager.registerSession(failedSession);
13430
- failedSession.emit("agent_event", guidance);
13579
+ const failedSessionId = createParams.existingSessionId ?? `failed-${Date.now()}`;
13431
13580
  this.eventBus.emit("agent:event", {
13432
- sessionId: failedSession.id,
13581
+ sessionId: failedSessionId,
13433
13582
  event: guidance
13434
13583
  });
13435
13584
  throw err;
@@ -13590,6 +13739,25 @@ var SessionFactory = class {
13590
13739
  session.setAgentCapabilities(record.acpState.agentCapabilities);
13591
13740
  }
13592
13741
  }
13742
+ const resumeFalledBack = record.agentSessionId && session.agentSessionId !== record.agentSessionId;
13743
+ if (resumeFalledBack) {
13744
+ log7.info({ sessionId: session.id }, "Resume fell back to fresh spawn \u2014 injecting conversation history");
13745
+ const contextManager = this.getContextManager?.();
13746
+ if (contextManager) {
13747
+ try {
13748
+ const config = this.configManager?.get();
13749
+ const labelAgent = config?.agentSwitch?.labelHistory ?? true;
13750
+ const contextResult = await contextManager.buildContext(
13751
+ { type: "session", value: record.sessionId, repoPath: record.workingDir },
13752
+ { labelAgent, noCache: true }
13753
+ );
13754
+ if (contextResult?.markdown) {
13755
+ session.setContext(contextResult.markdown);
13756
+ }
13757
+ } catch {
13758
+ }
13759
+ }
13760
+ }
13593
13761
  log7.info({ sessionId: session.id, threadId }, "Lazy resume successful");
13594
13762
  return session;
13595
13763
  } catch (err) {
@@ -13714,13 +13882,14 @@ var SessionFactory = class {
13714
13882
  };
13715
13883
 
13716
13884
  // src/core/core.ts
13717
- import path17 from "path";
13885
+ import path16 from "path";
13718
13886
  import os8 from "os";
13887
+ import { nanoid as nanoid3 } from "nanoid";
13719
13888
 
13720
13889
  // src/core/sessions/session-store.ts
13721
13890
  init_log();
13722
- import fs11 from "fs";
13723
- import path10 from "path";
13891
+ import fs10 from "fs";
13892
+ import path9 from "path";
13724
13893
  var log8 = createChildLogger({ module: "session-store" });
13725
13894
  var DEBOUNCE_MS = 2e3;
13726
13895
  var JsonFileSessionStore = class {
@@ -13794,9 +13963,9 @@ var JsonFileSessionStore = class {
13794
13963
  version: 1,
13795
13964
  sessions: Object.fromEntries(this.records)
13796
13965
  };
13797
- const dir = path10.dirname(this.filePath);
13798
- if (!fs11.existsSync(dir)) fs11.mkdirSync(dir, { recursive: true });
13799
- fs11.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
13966
+ const dir = path9.dirname(this.filePath);
13967
+ if (!fs10.existsSync(dir)) fs10.mkdirSync(dir, { recursive: true });
13968
+ fs10.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
13800
13969
  }
13801
13970
  destroy() {
13802
13971
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
@@ -13809,10 +13978,10 @@ var JsonFileSessionStore = class {
13809
13978
  }
13810
13979
  }
13811
13980
  load() {
13812
- if (!fs11.existsSync(this.filePath)) return;
13981
+ if (!fs10.existsSync(this.filePath)) return;
13813
13982
  try {
13814
13983
  const raw = JSON.parse(
13815
- fs11.readFileSync(this.filePath, "utf-8")
13984
+ fs10.readFileSync(this.filePath, "utf-8")
13816
13985
  );
13817
13986
  if (raw.version !== 1) {
13818
13987
  log8.warn(
@@ -13828,7 +13997,7 @@ var JsonFileSessionStore = class {
13828
13997
  } catch (err) {
13829
13998
  log8.error({ err }, "Failed to load session store, backing up corrupt file");
13830
13999
  try {
13831
- fs11.renameSync(this.filePath, `${this.filePath}.bak`);
14000
+ fs10.renameSync(this.filePath, `${this.filePath}.bak`);
13832
14001
  } catch {
13833
14002
  }
13834
14003
  }
@@ -13852,7 +14021,10 @@ var JsonFileSessionStore = class {
13852
14021
  for (const [id, record] of this.records) {
13853
14022
  if (record.status === "active" || record.status === "initializing")
13854
14023
  continue;
13855
- const lastActive = new Date(record.lastActiveAt).getTime();
14024
+ const raw = record.lastActiveAt;
14025
+ if (!raw) continue;
14026
+ const lastActive = new Date(raw).getTime();
14027
+ if (isNaN(lastActive)) continue;
13856
14028
  if (lastActive < cutoff) {
13857
14029
  this.records.delete(id);
13858
14030
  removed++;
@@ -13910,8 +14082,8 @@ var AgentSwitchHandler = class {
13910
14082
  if (middlewareChain && !result) throw new Error("Agent switch blocked by middleware");
13911
14083
  const lastEntry = session.findLastSwitchEntry(toAgent);
13912
14084
  const caps = getAgentCapabilities(toAgent);
13913
- const canResume = !!(lastEntry && caps.supportsResume && lastEntry.promptCount === 0);
13914
- const resumed = canResume;
14085
+ const canResume = !!(lastEntry && caps.supportsResume);
14086
+ let resumed = false;
13915
14087
  const startEvent = {
13916
14088
  type: "system_message",
13917
14089
  message: `Switching from ${fromAgent} to ${toAgent}...`
@@ -13945,29 +14117,34 @@ var AgentSwitchHandler = class {
13945
14117
  try {
13946
14118
  await session.switchAgent(toAgent, async () => {
13947
14119
  if (canResume) {
13948
- const instance = await agentManager.resume(toAgent, session.workingDirectory, lastEntry.agentSessionId);
13949
- if (fileService) instance.addAllowedPath(fileService.baseDir);
13950
- return instance;
13951
- } else {
13952
- const instance = await agentManager.spawn(toAgent, session.workingDirectory);
13953
- if (fileService) instance.addAllowedPath(fileService.baseDir);
13954
14120
  try {
13955
- const contextService = this.deps.getService("context");
13956
- if (contextService) {
13957
- const config = configManager.get();
13958
- const labelAgent = config.agentSwitch?.labelHistory ?? true;
13959
- const contextResult = await contextService.buildContext(
13960
- { type: "session", value: sessionId, repoPath: session.workingDirectory },
13961
- { labelAgent }
13962
- );
13963
- if (contextResult?.markdown) {
13964
- session.setContext(contextResult.markdown);
13965
- }
13966
- }
14121
+ const instance2 = await agentManager.resume(toAgent, session.workingDirectory, lastEntry.agentSessionId);
14122
+ if (fileService) instance2.addAllowedPath(fileService.baseDir);
14123
+ resumed = true;
14124
+ return instance2;
13967
14125
  } catch {
14126
+ log9.warn({ sessionId, toAgent }, "Resume failed, falling back to new agent with context injection");
14127
+ }
14128
+ }
14129
+ const instance = await agentManager.spawn(toAgent, session.workingDirectory);
14130
+ if (fileService) instance.addAllowedPath(fileService.baseDir);
14131
+ try {
14132
+ const contextService = this.deps.getService("context");
14133
+ if (contextService) {
14134
+ const config = configManager.get();
14135
+ const labelAgent = config.agentSwitch?.labelHistory ?? true;
14136
+ await contextService.flushSession(sessionId);
14137
+ const contextResult = await contextService.buildContext(
14138
+ { type: "session", value: sessionId, repoPath: session.workingDirectory },
14139
+ { labelAgent, noCache: true }
14140
+ );
14141
+ if (contextResult?.markdown) {
14142
+ session.setContext(contextResult.markdown);
14143
+ }
13968
14144
  }
13969
- return instance;
14145
+ } catch {
13970
14146
  }
14147
+ return instance;
13971
14148
  });
13972
14149
  const successEvent = {
13973
14150
  type: "system_message",
@@ -14051,14 +14228,14 @@ var AgentSwitchHandler = class {
14051
14228
  };
14052
14229
 
14053
14230
  // src/core/agents/agent-catalog.ts
14054
- import * as fs15 from "fs";
14055
- import * as path14 from "path";
14231
+ import * as fs14 from "fs";
14232
+ import * as path13 from "path";
14056
14233
  import * as os6 from "os";
14057
14234
 
14058
14235
  // src/core/agents/agent-store.ts
14059
14236
  init_log();
14060
- import * as fs13 from "fs";
14061
- import * as path12 from "path";
14237
+ import * as fs12 from "fs";
14238
+ import * as path11 from "path";
14062
14239
  import * as os4 from "os";
14063
14240
  import { z as z2 } from "zod";
14064
14241
  var log10 = createChildLogger({ module: "agent-store" });
@@ -14082,15 +14259,15 @@ var AgentStore = class {
14082
14259
  data = { version: 1, installed: {} };
14083
14260
  filePath;
14084
14261
  constructor(filePath) {
14085
- this.filePath = filePath ?? path12.join(os4.homedir(), ".openacp", "agents.json");
14262
+ this.filePath = filePath ?? path11.join(os4.homedir(), ".openacp", "agents.json");
14086
14263
  }
14087
14264
  load() {
14088
- if (!fs13.existsSync(this.filePath)) {
14265
+ if (!fs12.existsSync(this.filePath)) {
14089
14266
  this.data = { version: 1, installed: {} };
14090
14267
  return;
14091
14268
  }
14092
14269
  try {
14093
- const raw = JSON.parse(fs13.readFileSync(this.filePath, "utf-8"));
14270
+ const raw = JSON.parse(fs12.readFileSync(this.filePath, "utf-8"));
14094
14271
  const result = AgentStoreSchema.safeParse(raw);
14095
14272
  if (result.success) {
14096
14273
  this.data = result.data;
@@ -14104,7 +14281,7 @@ var AgentStore = class {
14104
14281
  }
14105
14282
  }
14106
14283
  exists() {
14107
- return fs13.existsSync(this.filePath);
14284
+ return fs12.existsSync(this.filePath);
14108
14285
  }
14109
14286
  getInstalled() {
14110
14287
  return this.data.installed;
@@ -14124,10 +14301,10 @@ var AgentStore = class {
14124
14301
  return key in this.data.installed;
14125
14302
  }
14126
14303
  save() {
14127
- fs13.mkdirSync(path12.dirname(this.filePath), { recursive: true });
14304
+ fs12.mkdirSync(path11.dirname(this.filePath), { recursive: true });
14128
14305
  const tmpPath = this.filePath + ".tmp";
14129
- fs13.writeFileSync(tmpPath, JSON.stringify(this.data, null, 2), { mode: 384 });
14130
- fs13.renameSync(tmpPath, this.filePath);
14306
+ fs12.writeFileSync(tmpPath, JSON.stringify(this.data, null, 2), { mode: 384 });
14307
+ fs12.renameSync(tmpPath, this.filePath);
14131
14308
  }
14132
14309
  };
14133
14310
 
@@ -14145,7 +14322,7 @@ var AgentCatalog = class {
14145
14322
  agentsDir;
14146
14323
  constructor(store, cachePath, agentsDir) {
14147
14324
  this.store = store ?? new AgentStore();
14148
- this.cachePath = cachePath ?? path14.join(os6.homedir(), ".openacp", "registry-cache.json");
14325
+ this.cachePath = cachePath ?? path13.join(os6.homedir(), ".openacp", "registry-cache.json");
14149
14326
  this.agentsDir = agentsDir;
14150
14327
  }
14151
14328
  load() {
@@ -14166,8 +14343,8 @@ var AgentCatalog = class {
14166
14343
  ttlHours: DEFAULT_TTL_HOURS,
14167
14344
  data
14168
14345
  };
14169
- fs15.mkdirSync(path14.dirname(this.cachePath), { recursive: true });
14170
- fs15.writeFileSync(this.cachePath, JSON.stringify(cache, null, 2), { mode: 384 });
14346
+ fs14.mkdirSync(path13.dirname(this.cachePath), { recursive: true });
14347
+ fs14.writeFileSync(this.cachePath, JSON.stringify(cache, null, 2), { mode: 384 });
14171
14348
  log12.info({ count: this.registryAgents.length }, "Registry updated");
14172
14349
  } catch (err) {
14173
14350
  log12.warn({ err }, "Failed to fetch registry, using cached data");
@@ -14335,9 +14512,9 @@ var AgentCatalog = class {
14335
14512
  }
14336
14513
  }
14337
14514
  isCacheStale() {
14338
- if (!fs15.existsSync(this.cachePath)) return true;
14515
+ if (!fs14.existsSync(this.cachePath)) return true;
14339
14516
  try {
14340
- const raw = JSON.parse(fs15.readFileSync(this.cachePath, "utf-8"));
14517
+ const raw = JSON.parse(fs14.readFileSync(this.cachePath, "utf-8"));
14341
14518
  const fetchedAt = new Date(raw.fetchedAt).getTime();
14342
14519
  const ttlMs = (raw.ttlHours ?? DEFAULT_TTL_HOURS) * 60 * 60 * 1e3;
14343
14520
  return Date.now() - fetchedAt > ttlMs;
@@ -14346,9 +14523,9 @@ var AgentCatalog = class {
14346
14523
  }
14347
14524
  }
14348
14525
  loadRegistryFromCacheOrSnapshot() {
14349
- if (fs15.existsSync(this.cachePath)) {
14526
+ if (fs14.existsSync(this.cachePath)) {
14350
14527
  try {
14351
- const raw = JSON.parse(fs15.readFileSync(this.cachePath, "utf-8"));
14528
+ const raw = JSON.parse(fs14.readFileSync(this.cachePath, "utf-8"));
14352
14529
  if (raw.data?.agents) {
14353
14530
  this.registryAgents = raw.data.agents;
14354
14531
  log12.debug({ count: this.registryAgents.length }, "Loaded registry from cache");
@@ -14360,13 +14537,13 @@ var AgentCatalog = class {
14360
14537
  }
14361
14538
  try {
14362
14539
  const candidates = [
14363
- path14.join(import.meta.dirname, "data", "registry-snapshot.json"),
14364
- path14.join(import.meta.dirname, "..", "data", "registry-snapshot.json"),
14365
- path14.join(import.meta.dirname, "..", "..", "data", "registry-snapshot.json")
14540
+ path13.join(import.meta.dirname, "data", "registry-snapshot.json"),
14541
+ path13.join(import.meta.dirname, "..", "data", "registry-snapshot.json"),
14542
+ path13.join(import.meta.dirname, "..", "..", "data", "registry-snapshot.json")
14366
14543
  ];
14367
14544
  for (const candidate of candidates) {
14368
- if (fs15.existsSync(candidate)) {
14369
- const raw = JSON.parse(fs15.readFileSync(candidate, "utf-8"));
14545
+ if (fs14.existsSync(candidate)) {
14546
+ const raw = JSON.parse(fs14.readFileSync(candidate, "utf-8"));
14370
14547
  this.registryAgents = raw.agents ?? [];
14371
14548
  log12.debug({ count: this.registryAgents.length }, "Loaded registry from bundled snapshot");
14372
14549
  return;
@@ -14627,31 +14804,31 @@ var ErrorTracker = class {
14627
14804
  };
14628
14805
 
14629
14806
  // src/core/plugin/plugin-context.ts
14630
- import path16 from "path";
14807
+ import path15 from "path";
14631
14808
  import os7 from "os";
14632
14809
 
14633
14810
  // src/core/plugin/plugin-storage.ts
14634
- import fs16 from "fs";
14635
- import path15 from "path";
14811
+ import fs15 from "fs";
14812
+ import path14 from "path";
14636
14813
  var PluginStorageImpl = class {
14637
14814
  kvPath;
14638
14815
  dataDir;
14639
14816
  writeChain = Promise.resolve();
14640
14817
  constructor(baseDir) {
14641
- this.dataDir = path15.join(baseDir, "data");
14642
- this.kvPath = path15.join(baseDir, "kv.json");
14643
- fs16.mkdirSync(baseDir, { recursive: true });
14818
+ this.dataDir = path14.join(baseDir, "data");
14819
+ this.kvPath = path14.join(baseDir, "kv.json");
14820
+ fs15.mkdirSync(baseDir, { recursive: true });
14644
14821
  }
14645
14822
  readKv() {
14646
14823
  try {
14647
- const raw = fs16.readFileSync(this.kvPath, "utf-8");
14824
+ const raw = fs15.readFileSync(this.kvPath, "utf-8");
14648
14825
  return JSON.parse(raw);
14649
14826
  } catch {
14650
14827
  return {};
14651
14828
  }
14652
14829
  }
14653
14830
  writeKv(data) {
14654
- fs16.writeFileSync(this.kvPath, JSON.stringify(data), "utf-8");
14831
+ fs15.writeFileSync(this.kvPath, JSON.stringify(data), "utf-8");
14655
14832
  }
14656
14833
  async get(key) {
14657
14834
  const data = this.readKv();
@@ -14677,7 +14854,7 @@ var PluginStorageImpl = class {
14677
14854
  return Object.keys(this.readKv());
14678
14855
  }
14679
14856
  getDataDir() {
14680
- fs16.mkdirSync(this.dataDir, { recursive: true });
14857
+ fs15.mkdirSync(this.dataDir, { recursive: true });
14681
14858
  return this.dataDir;
14682
14859
  }
14683
14860
  };
@@ -14701,9 +14878,11 @@ function createPluginContext(opts) {
14701
14878
  config,
14702
14879
  core
14703
14880
  } = opts;
14704
- const instanceRoot = opts.instanceRoot ?? path16.join(os7.homedir(), ".openacp");
14881
+ const instanceRoot = opts.instanceRoot ?? path15.join(os7.homedir(), ".openacp");
14705
14882
  const registeredListeners = [];
14706
14883
  const registeredCommands = [];
14884
+ const registeredMenuItemIds = [];
14885
+ const registeredAssistantSectionIds = [];
14707
14886
  const noopLog = {
14708
14887
  trace() {
14709
14888
  },
@@ -14798,7 +14977,9 @@ function createPluginContext(opts) {
14798
14977
  requirePermission(permissions, "commands:register", "registerMenuItem()");
14799
14978
  const menuRegistry = serviceRegistry.get("menu-registry");
14800
14979
  if (!menuRegistry) return;
14801
- menuRegistry.register({ ...item, id: `${pluginName}:${item.id}` });
14980
+ const qualifiedId = `${pluginName}:${item.id}`;
14981
+ menuRegistry.register({ ...item, id: qualifiedId });
14982
+ registeredMenuItemIds.push(qualifiedId);
14802
14983
  },
14803
14984
  unregisterMenuItem(id) {
14804
14985
  requirePermission(permissions, "commands:register", "unregisterMenuItem()");
@@ -14810,7 +14991,9 @@ function createPluginContext(opts) {
14810
14991
  requirePermission(permissions, "commands:register", "registerAssistantSection()");
14811
14992
  const assistantRegistry = serviceRegistry.get("assistant-registry");
14812
14993
  if (!assistantRegistry) return;
14813
- assistantRegistry.register({ ...section, id: `${pluginName}:${section.id}` });
14994
+ const qualifiedId = `${pluginName}:${section.id}`;
14995
+ assistantRegistry.register({ ...section, id: qualifiedId });
14996
+ registeredAssistantSectionIds.push(qualifiedId);
14814
14997
  },
14815
14998
  unregisterAssistantSection(id) {
14816
14999
  requirePermission(permissions, "commands:register", "unregisterAssistantSection()");
@@ -14818,6 +15001,14 @@ function createPluginContext(opts) {
14818
15001
  if (!assistantRegistry) return;
14819
15002
  assistantRegistry.unregister(`${pluginName}:${id}`);
14820
15003
  },
15004
+ registerEditableFields(fields) {
15005
+ requirePermission(permissions, "commands:register", "registerEditableFields()");
15006
+ const registry = serviceRegistry.get("field-registry");
15007
+ if (registry && typeof registry.register === "function") {
15008
+ registry.register(pluginName, fields);
15009
+ log34.debug(`Registered ${fields.length} editable field(s) for ${pluginName}`);
15010
+ }
15011
+ },
14821
15012
  get sessions() {
14822
15013
  requirePermission(permissions, "kernel:access", "sessions");
14823
15014
  return sessions;
@@ -14846,6 +15037,20 @@ function createPluginContext(opts) {
14846
15037
  if (cmdRegistry && typeof cmdRegistry.unregisterByPlugin === "function") {
14847
15038
  cmdRegistry.unregisterByPlugin(pluginName);
14848
15039
  }
15040
+ const menuRegistry = serviceRegistry.get("menu-registry");
15041
+ if (menuRegistry) {
15042
+ for (const id of registeredMenuItemIds) {
15043
+ menuRegistry.unregister(id);
15044
+ }
15045
+ }
15046
+ registeredMenuItemIds.length = 0;
15047
+ const assistantRegistry = serviceRegistry.get("assistant-registry");
15048
+ if (assistantRegistry) {
15049
+ for (const id of registeredAssistantSectionIds) {
15050
+ assistantRegistry.unregister(id);
15051
+ }
15052
+ }
15053
+ registeredAssistantSectionIds.length = 0;
14849
15054
  registeredCommands.length = 0;
14850
15055
  }
14851
15056
  };
@@ -14880,11 +15085,8 @@ function resolvePluginConfig(pluginName, configManager) {
14880
15085
  "@openacp/file-service": "files",
14881
15086
  "@openacp/api-server": "api",
14882
15087
  "@openacp/telegram": "channels.telegram",
14883
- "@openacp/discord": "channels.discord",
14884
- "@openacp/adapter-discord": "channels.discord",
14885
- "@openacp/plugin-discord": "channels.discord",
14886
- // alias for old name
14887
- "@openacp/slack": "channels.slack"
15088
+ "@openacp/discord-adapter": "channels.discord",
15089
+ "@openacp/slack-adapter": "channels.slack"
14888
15090
  };
14889
15091
  const legacyKey = legacyMap[pluginName];
14890
15092
  if (legacyKey) {
@@ -15369,8 +15571,10 @@ function createConfigSection(core) {
15369
15571
  priority: 30,
15370
15572
  buildContext: () => {
15371
15573
  const config = core.configManager.get();
15574
+ const speechSvc = core.lifecycleManager?.serviceRegistry.get("speech");
15575
+ const sttActive = speechSvc ? speechSvc.isSTTAvailable() : false;
15372
15576
  return `Workspace base: ${config.workspace.baseDir}
15373
- STT: ${config.speech?.stt?.provider ? `${config.speech.stt.provider} \u2705` : "Not configured"}`;
15577
+ STT: ${sttActive ? "configured \u2705" : "Not configured"}`;
15374
15578
  },
15375
15579
  commands: [
15376
15580
  { command: "openacp config", description: "View config" },
@@ -15531,7 +15735,7 @@ var OpenACPCore = class {
15531
15735
  );
15532
15736
  this.agentCatalog.load();
15533
15737
  this.agentManager = new AgentManager(this.agentCatalog);
15534
- const storePath = ctx?.paths.sessions ?? path17.join(os8.homedir(), ".openacp", "sessions.json");
15738
+ const storePath = ctx?.paths.sessions ?? path16.join(os8.homedir(), ".openacp", "sessions.json");
15535
15739
  this.sessionStore = new JsonFileSessionStore(
15536
15740
  storePath,
15537
15741
  config.sessionStore.ttlDays
@@ -15555,7 +15759,7 @@ var OpenACPCore = class {
15555
15759
  sessions: this.sessionManager,
15556
15760
  config: this.configManager,
15557
15761
  core: this,
15558
- storagePath: ctx?.paths.pluginsData ?? path17.join(os8.homedir(), ".openacp", "plugins", "data"),
15762
+ storagePath: ctx?.paths.pluginsData ?? path16.join(os8.homedir(), ".openacp", "plugins", "data"),
15559
15763
  instanceRoot: ctx?.root,
15560
15764
  log: createChildLogger({ module: "plugin" })
15561
15765
  });
@@ -15621,7 +15825,7 @@ var OpenACPCore = class {
15621
15825
  );
15622
15826
  registerCoreMenuItems(this.menuRegistry);
15623
15827
  if (ctx?.root) {
15624
- this.assistantRegistry.setInstanceRoot(path17.dirname(ctx.root));
15828
+ this.assistantRegistry.setInstanceRoot(path16.dirname(ctx.root));
15625
15829
  }
15626
15830
  this.assistantRegistry.register(createSessionsSection(this));
15627
15831
  this.assistantRegistry.register(createAgentsSection(this));
@@ -15759,7 +15963,22 @@ User message:
15759
15963
  ${text3}`;
15760
15964
  }
15761
15965
  }
15762
- await session.enqueuePrompt(text3, message.attachments, message.routing);
15966
+ const sourceAdapterId = message.routing?.sourceAdapterId ?? message.channelId;
15967
+ if (sourceAdapterId && sourceAdapterId !== "sse" && sourceAdapterId !== "api") {
15968
+ const turnId = nanoid3(8);
15969
+ this.eventBus.emit("message:queued", {
15970
+ sessionId: session.id,
15971
+ turnId,
15972
+ text: text3,
15973
+ sourceAdapterId,
15974
+ attachments: message.attachments,
15975
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
15976
+ queueDepth: session.queueDepth
15977
+ });
15978
+ await session.enqueuePrompt(text3, message.attachments, message.routing, turnId);
15979
+ } else {
15980
+ await session.enqueuePrompt(text3, message.attachments, message.routing);
15981
+ }
15763
15982
  }
15764
15983
  // --- Unified Session Creation Pipeline ---
15765
15984
  async createSession(params) {
@@ -15873,15 +16092,15 @@ ${text3}`;
15873
16092
  message: `Agent '${agentName}' not found`
15874
16093
  };
15875
16094
  }
15876
- const { existsSync: existsSync20 } = await import("fs");
15877
- if (!existsSync20(cwd)) {
16095
+ const { existsSync: existsSync19 } = await import("fs");
16096
+ if (!existsSync19(cwd)) {
15878
16097
  return {
15879
16098
  ok: false,
15880
16099
  error: "invalid_cwd",
15881
16100
  message: `Directory does not exist: ${cwd}`
15882
16101
  };
15883
16102
  }
15884
- const maxSessions = this.configManager.get().security.maxConcurrentSessions;
16103
+ const maxSessions = 20;
15885
16104
  if (this.sessionManager.listSessions().length >= maxSessions) {
15886
16105
  return {
15887
16106
  ok: false,
@@ -16138,7 +16357,9 @@ var CommandRegistry = class _CommandRegistry {
16138
16357
  this.commands.delete(name);
16139
16358
  if (cmd.scope) {
16140
16359
  this.commands.delete(`${cmd.scope}:${cmd.name}`);
16141
- this.commands.delete(cmd.name);
16360
+ if (this.commands.get(cmd.name) === cmd) {
16361
+ this.commands.delete(cmd.name);
16362
+ }
16142
16363
  }
16143
16364
  }
16144
16365
  /** Remove all commands registered by a given plugin. */
@@ -16218,19 +16439,19 @@ init_doctor();
16218
16439
  init_config_registry();
16219
16440
 
16220
16441
  // src/core/config/config-editor.ts
16221
- import * as path29 from "path";
16442
+ import * as path28 from "path";
16222
16443
  import * as clack2 from "@clack/prompts";
16223
16444
 
16224
16445
  // src/cli/autostart.ts
16225
16446
  init_log();
16226
- import { execFileSync as execFileSync5 } from "child_process";
16227
- import * as fs27 from "fs";
16228
- import * as path25 from "path";
16447
+ import { execFileSync as execFileSync6 } from "child_process";
16448
+ import * as fs26 from "fs";
16449
+ import * as path24 from "path";
16229
16450
  import * as os11 from "os";
16230
16451
  var log18 = createChildLogger({ module: "autostart" });
16231
16452
  var LAUNCHD_LABEL = "com.openacp.daemon";
16232
- var LAUNCHD_PLIST_PATH = path25.join(os11.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
16233
- var SYSTEMD_SERVICE_PATH = path25.join(os11.homedir(), ".config", "systemd", "user", "openacp.service");
16453
+ var LAUNCHD_PLIST_PATH = path24.join(os11.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
16454
+ var SYSTEMD_SERVICE_PATH = path24.join(os11.homedir(), ".config", "systemd", "user", "openacp.service");
16234
16455
  function isAutoStartSupported() {
16235
16456
  return process.platform === "darwin" || process.platform === "linux";
16236
16457
  }
@@ -16242,7 +16463,7 @@ function escapeSystemdValue(str) {
16242
16463
  return `"${escaped}"`;
16243
16464
  }
16244
16465
  function generateLaunchdPlist(nodePath, cliPath, logDir2) {
16245
- const logFile = path25.join(logDir2, "openacp.log");
16466
+ const logFile = path24.join(logDir2, "openacp.log");
16246
16467
  return `<?xml version="1.0" encoding="UTF-8"?>
16247
16468
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
16248
16469
  <plist version="1.0">
@@ -16287,25 +16508,25 @@ function installAutoStart(logDir2) {
16287
16508
  return { success: false, error: "Auto-start not supported on this platform" };
16288
16509
  }
16289
16510
  const nodePath = process.execPath;
16290
- const cliPath = path25.resolve(process.argv[1]);
16291
- const resolvedLogDir = logDir2.startsWith("~") ? path25.join(os11.homedir(), logDir2.slice(1)) : logDir2;
16511
+ const cliPath = path24.resolve(process.argv[1]);
16512
+ const resolvedLogDir = logDir2.startsWith("~") ? path24.join(os11.homedir(), logDir2.slice(1)) : logDir2;
16292
16513
  try {
16293
16514
  if (process.platform === "darwin") {
16294
16515
  const plist = generateLaunchdPlist(nodePath, cliPath, resolvedLogDir);
16295
- const dir = path25.dirname(LAUNCHD_PLIST_PATH);
16296
- fs27.mkdirSync(dir, { recursive: true });
16297
- fs27.writeFileSync(LAUNCHD_PLIST_PATH, plist);
16298
- execFileSync5("launchctl", ["load", LAUNCHD_PLIST_PATH], { stdio: "pipe" });
16516
+ const dir = path24.dirname(LAUNCHD_PLIST_PATH);
16517
+ fs26.mkdirSync(dir, { recursive: true });
16518
+ fs26.writeFileSync(LAUNCHD_PLIST_PATH, plist);
16519
+ execFileSync6("launchctl", ["load", LAUNCHD_PLIST_PATH], { stdio: "pipe" });
16299
16520
  log18.info("LaunchAgent installed");
16300
16521
  return { success: true };
16301
16522
  }
16302
16523
  if (process.platform === "linux") {
16303
16524
  const unit = generateSystemdUnit(nodePath, cliPath);
16304
- const dir = path25.dirname(SYSTEMD_SERVICE_PATH);
16305
- fs27.mkdirSync(dir, { recursive: true });
16306
- fs27.writeFileSync(SYSTEMD_SERVICE_PATH, unit);
16307
- execFileSync5("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
16308
- execFileSync5("systemctl", ["--user", "enable", "openacp"], { stdio: "pipe" });
16525
+ const dir = path24.dirname(SYSTEMD_SERVICE_PATH);
16526
+ fs26.mkdirSync(dir, { recursive: true });
16527
+ fs26.writeFileSync(SYSTEMD_SERVICE_PATH, unit);
16528
+ execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
16529
+ execFileSync6("systemctl", ["--user", "enable", "openacp"], { stdio: "pipe" });
16309
16530
  log18.info("systemd user service installed");
16310
16531
  return { success: true };
16311
16532
  }
@@ -16322,24 +16543,24 @@ function uninstallAutoStart() {
16322
16543
  }
16323
16544
  try {
16324
16545
  if (process.platform === "darwin") {
16325
- if (fs27.existsSync(LAUNCHD_PLIST_PATH)) {
16546
+ if (fs26.existsSync(LAUNCHD_PLIST_PATH)) {
16326
16547
  try {
16327
- execFileSync5("launchctl", ["unload", LAUNCHD_PLIST_PATH], { stdio: "pipe" });
16548
+ execFileSync6("launchctl", ["unload", LAUNCHD_PLIST_PATH], { stdio: "pipe" });
16328
16549
  } catch {
16329
16550
  }
16330
- fs27.unlinkSync(LAUNCHD_PLIST_PATH);
16551
+ fs26.unlinkSync(LAUNCHD_PLIST_PATH);
16331
16552
  log18.info("LaunchAgent removed");
16332
16553
  }
16333
16554
  return { success: true };
16334
16555
  }
16335
16556
  if (process.platform === "linux") {
16336
- if (fs27.existsSync(SYSTEMD_SERVICE_PATH)) {
16557
+ if (fs26.existsSync(SYSTEMD_SERVICE_PATH)) {
16337
16558
  try {
16338
- execFileSync5("systemctl", ["--user", "disable", "openacp"], { stdio: "pipe" });
16559
+ execFileSync6("systemctl", ["--user", "disable", "openacp"], { stdio: "pipe" });
16339
16560
  } catch {
16340
16561
  }
16341
- fs27.unlinkSync(SYSTEMD_SERVICE_PATH);
16342
- execFileSync5("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
16562
+ fs26.unlinkSync(SYSTEMD_SERVICE_PATH);
16563
+ execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
16343
16564
  log18.info("systemd user service removed");
16344
16565
  }
16345
16566
  return { success: true };
@@ -16353,10 +16574,10 @@ function uninstallAutoStart() {
16353
16574
  }
16354
16575
  function isAutoStartInstalled() {
16355
16576
  if (process.platform === "darwin") {
16356
- return fs27.existsSync(LAUNCHD_PLIST_PATH);
16577
+ return fs26.existsSync(LAUNCHD_PLIST_PATH);
16357
16578
  }
16358
16579
  if (process.platform === "linux") {
16359
- return fs27.existsSync(SYSTEMD_SERVICE_PATH);
16580
+ return fs26.existsSync(SYSTEMD_SERVICE_PATH);
16360
16581
  }
16361
16582
  return false;
16362
16583
  }
@@ -16407,42 +16628,19 @@ var header = (title) => `
16407
16628
  ${c.cyan}${c.bold}[${title}]${c.reset}
16408
16629
  `;
16409
16630
  async function editTelegram(config, updates, settingsManager) {
16410
- const tg = config.channels?.telegram ?? {};
16411
- let currentToken = tg.botToken ?? "";
16412
- let currentChatId = tg.chatId ?? 0;
16413
- let currentEnabled = tg.enabled ?? false;
16414
- if (settingsManager) {
16415
- const ps = await settingsManager.loadSettings("@openacp/telegram");
16416
- if (Object.keys(ps).length > 0) {
16417
- currentToken = ps.botToken ?? currentToken;
16418
- currentChatId = ps.chatId ?? currentChatId;
16419
- currentEnabled = ps.enabled ?? currentEnabled;
16420
- }
16421
- }
16631
+ const ps = settingsManager ? await settingsManager.loadSettings("@openacp/telegram") : {};
16632
+ const currentToken = ps.botToken ?? "";
16633
+ const currentChatId = ps.chatId ?? 0;
16634
+ const currentEnabled = ps.enabled ?? false;
16422
16635
  console.log(header("Telegram"));
16423
- console.log(` Enabled : ${currentEnabled ? ok("yes") : dim("no")}`);
16424
16636
  const tokenDisplay = currentToken.length > 12 ? currentToken.slice(0, 6) + "..." + currentToken.slice(-6) : currentToken || dim("(not set)");
16637
+ console.log(` Enabled : ${currentEnabled ? ok("yes") : dim("no")}`);
16425
16638
  console.log(` Bot Token : ${tokenDisplay}`);
16426
16639
  console.log(` Chat ID : ${currentChatId || dim("(not set)")}`);
16427
16640
  console.log("");
16428
- const ensureTelegramUpdates = () => {
16429
- if (!updates.channels) updates.channels = {};
16430
- if (!updates.channels.telegram) {
16431
- updates.channels.telegram = {};
16432
- }
16433
- return updates.channels.telegram;
16434
- };
16435
16641
  while (true) {
16436
- const isEnabled = await (async () => {
16437
- if (settingsManager) {
16438
- const ps = await settingsManager.loadSettings("@openacp/telegram");
16439
- if ("enabled" in ps) return ps.enabled;
16440
- }
16441
- const ch = updates.channels;
16442
- const tgUp = ch?.telegram;
16443
- if (tgUp && "enabled" in tgUp) return tgUp.enabled;
16444
- return currentEnabled;
16445
- })();
16642
+ const freshSettings = settingsManager ? await settingsManager.loadSettings("@openacp/telegram") : ps;
16643
+ const isEnabled = freshSettings.enabled ?? currentEnabled;
16446
16644
  const choice = await select3({
16447
16645
  message: "Telegram settings:",
16448
16646
  choices: [
@@ -16456,75 +16654,32 @@ async function editTelegram(config, updates, settingsManager) {
16456
16654
  if (choice === "toggle") {
16457
16655
  if (settingsManager) {
16458
16656
  await settingsManager.updatePluginSettings("@openacp/telegram", { enabled: !isEnabled });
16459
- } else {
16460
- const tgUp = ensureTelegramUpdates();
16461
- tgUp.enabled = !isEnabled;
16657
+ console.log(!isEnabled ? ok("Telegram enabled") : ok("Telegram disabled"));
16462
16658
  }
16463
- console.log(!isEnabled ? ok("Telegram enabled") : ok("Telegram disabled"));
16464
16659
  }
16465
16660
  if (choice === "token") {
16466
16661
  const token = await input({
16467
16662
  message: "New bot token:",
16468
- default: currentToken,
16469
16663
  validate: (val) => val.trim().length > 0 || "Token cannot be empty"
16470
16664
  });
16471
- try {
16472
- const { validateBotToken: validateBotToken2 } = await Promise.resolve().then(() => (init_validators(), validators_exports));
16473
- const result = await validateBotToken2(token.trim());
16474
- if (result.ok) {
16475
- console.log(ok(`Connected to @${result.botUsername}`));
16476
- } else {
16477
- console.log(warn(`Validation failed: ${result.error} \u2014 saving anyway`));
16478
- }
16479
- } catch {
16480
- console.log(warn("Telegram validator not available \u2014 skipping validation"));
16481
- }
16482
16665
  if (settingsManager) {
16483
- await settingsManager.updatePluginSettings("@openacp/telegram", { botToken: token.trim(), enabled: true });
16484
- } else {
16485
- const tgUp = ensureTelegramUpdates();
16486
- tgUp.botToken = token.trim();
16487
- tgUp.enabled = true;
16666
+ await settingsManager.updatePluginSettings("@openacp/telegram", { botToken: token.trim() });
16667
+ console.log(ok("Bot token updated"));
16488
16668
  }
16489
16669
  }
16490
16670
  if (choice === "chatid") {
16491
- const chatIdStr = await input({
16492
- message: "New chat ID (e.g. -1001234567890):",
16493
- default: String(currentChatId),
16494
- validate: (val) => {
16495
- const n = Number(val.trim());
16496
- if (isNaN(n) || !Number.isInteger(n)) return "Chat ID must be an integer";
16497
- return true;
16498
- }
16671
+ const chatId = await input({
16672
+ message: "New chat ID:",
16673
+ validate: (val) => !isNaN(Number(val.trim())) || "Must be a number"
16499
16674
  });
16500
- const chatId = Number(chatIdStr.trim());
16501
- const tokenForValidation = (() => {
16502
- const ch = updates.channels;
16503
- const tgUp = ch?.telegram;
16504
- if (typeof tgUp?.botToken === "string") return tgUp.botToken;
16505
- return currentToken;
16506
- })();
16507
- try {
16508
- const { validateChatId: validateChatId2 } = await Promise.resolve().then(() => (init_validators(), validators_exports));
16509
- const result = await validateChatId2(tokenForValidation, chatId);
16510
- if (result.ok) {
16511
- console.log(ok(`Group: ${result.title}${result.isForum ? "" : warn(" (topics not enabled)")}`));
16512
- } else {
16513
- console.log(warn(`Validation failed: ${result.error} \u2014 saving anyway`));
16514
- }
16515
- } catch {
16516
- console.log(warn("Telegram validator not available \u2014 skipping validation"));
16517
- }
16518
16675
  if (settingsManager) {
16519
- await settingsManager.updatePluginSettings("@openacp/telegram", { chatId });
16520
- } else {
16521
- const tgUp = ensureTelegramUpdates();
16522
- tgUp.chatId = chatId;
16676
+ await settingsManager.updatePluginSettings("@openacp/telegram", { chatId: Number(chatId.trim()) });
16677
+ console.log(ok(`Chat ID set to ${chatId.trim()}`));
16523
16678
  }
16524
16679
  }
16525
16680
  }
16526
16681
  }
16527
- var DISCORD_PACKAGE = "@openacp/adapter-discord";
16682
+ var DISCORD_PACKAGE = "@openacp/discord-adapter";
16528
16683
  async function ensureDiscordPlugin() {
16529
16684
  try {
16530
16685
  return await import(DISCORD_PACKAGE);
@@ -16561,7 +16716,7 @@ async function editDiscord(_config, _updates) {
16561
16716
  const { createInstallContext: createInstallContext2 } = await Promise.resolve().then(() => (init_install_context(), install_context_exports));
16562
16717
  const { getGlobalRoot: getGlobalRoot2 } = await Promise.resolve().then(() => (init_instance_context(), instance_context_exports));
16563
16718
  const root = getGlobalRoot2();
16564
- const basePath = path29.join(root, "plugins", "data");
16719
+ const basePath = path28.join(root, "plugins", "data");
16565
16720
  const settingsManager = new SettingsManager2(basePath);
16566
16721
  const ctx = createInstallContext2({
16567
16722
  pluginName: plugin.name,
@@ -16575,12 +16730,12 @@ async function editDiscord(_config, _updates) {
16575
16730
  }
16576
16731
  }
16577
16732
  async function editChannels(config, updates, settingsManager) {
16578
- let tgConfigured = !!config.channels?.telegram;
16579
- let dcConfigured = !!config.channels?.discord;
16733
+ let tgConfigured = false;
16734
+ let dcConfigured = false;
16580
16735
  if (settingsManager) {
16581
16736
  const tgPs = await settingsManager.loadSettings("@openacp/telegram");
16582
16737
  if (tgPs.botToken && tgPs.chatId) tgConfigured = true;
16583
- const dcPs = await settingsManager.loadSettings("@openacp/adapter-discord");
16738
+ const dcPs = await settingsManager.loadSettings("@openacp/discord-adapter");
16584
16739
  if (dcPs.guildId || dcPs.token) dcConfigured = true;
16585
16740
  }
16586
16741
  console.log(header("Channels"));
@@ -16602,7 +16757,7 @@ async function editChannels(config, updates, settingsManager) {
16602
16757
  }
16603
16758
  }
16604
16759
  async function editAgent(config, updates) {
16605
- const agentNames = Object.keys(config.agents ?? {});
16760
+ const agentNames = [];
16606
16761
  const currentDefault = config.defaultAgent;
16607
16762
  console.log(header("Agent"));
16608
16763
  console.log(` Default agent : ${c.bold}${currentDefault}${c.reset}`);
@@ -16645,17 +16800,12 @@ async function editWorkspace(config, updates) {
16645
16800
  console.log(ok(`Workspace set to ${newDir.trim()}`));
16646
16801
  }
16647
16802
  async function editSecurity(config, updates, settingsManager) {
16648
- let sec = config.security ?? { allowedUserIds: [], maxConcurrentSessions: 20, sessionTimeoutMinutes: 60 };
16649
- if (settingsManager) {
16650
- const ps = await settingsManager.loadSettings("@openacp/security");
16651
- if (Object.keys(ps).length > 0) {
16652
- sec = {
16653
- allowedUserIds: ps.allowedUserIds ?? sec.allowedUserIds,
16654
- maxConcurrentSessions: ps.maxConcurrentSessions ?? sec.maxConcurrentSessions,
16655
- sessionTimeoutMinutes: ps.sessionTimeoutMinutes ?? sec.sessionTimeoutMinutes
16656
- };
16657
- }
16658
- }
16803
+ const ps = settingsManager ? await settingsManager.loadSettings("@openacp/security") : {};
16804
+ const sec = {
16805
+ allowedUserIds: ps.allowedUserIds ?? [],
16806
+ maxConcurrentSessions: ps.maxConcurrentSessions ?? 20,
16807
+ sessionTimeoutMinutes: ps.sessionTimeoutMinutes ?? 60
16808
+ };
16659
16809
  console.log(header("Security"));
16660
16810
  console.log(` Allowed user IDs : ${sec.allowedUserIds?.length ? sec.allowedUserIds.join(", ") : dim("(all users allowed)")}`);
16661
16811
  console.log(` Max concurrent sessions : ${sec.maxConcurrentSessions}`);
@@ -16683,9 +16833,6 @@ async function editSecurity(config, updates, settingsManager) {
16683
16833
  });
16684
16834
  if (settingsManager) {
16685
16835
  await settingsManager.updatePluginSettings("@openacp/security", { maxConcurrentSessions: Number(val.trim()) });
16686
- } else {
16687
- if (!updates.security) updates.security = {};
16688
- updates.security.maxConcurrentSessions = Number(val.trim());
16689
16836
  }
16690
16837
  console.log(ok(`Max concurrent sessions set to ${val.trim()}`));
16691
16838
  }
@@ -16701,9 +16848,6 @@ async function editSecurity(config, updates, settingsManager) {
16701
16848
  });
16702
16849
  if (settingsManager) {
16703
16850
  await settingsManager.updatePluginSettings("@openacp/security", { sessionTimeoutMinutes: Number(val.trim()) });
16704
- } else {
16705
- if (!updates.security) updates.security = {};
16706
- updates.security.sessionTimeoutMinutes = Number(val.trim());
16707
16851
  }
16708
16852
  console.log(ok(`Session timeout set to ${val.trim()} minutes`));
16709
16853
  }
@@ -16830,20 +16974,16 @@ async function editRunMode(config, updates) {
16830
16974
  }
16831
16975
  }
16832
16976
  async function editApi(config, updates, settingsManager) {
16833
- let api = config.api ?? { port: 21420, host: "127.0.0.1" };
16834
- if (settingsManager) {
16835
- const ps = await settingsManager.loadSettings("@openacp/api-server");
16836
- if (Object.keys(ps).length > 0) {
16837
- api = { port: ps.port ?? api.port, host: ps.host ?? api.host };
16838
- }
16839
- }
16977
+ const ps = settingsManager ? await settingsManager.loadSettings("@openacp/api-server") : {};
16978
+ const currentPort = ps.port ?? 21420;
16979
+ const currentHost = ps.host ?? "127.0.0.1";
16840
16980
  console.log(header("API"));
16841
- console.log(` Port : ${api.port}`);
16842
- console.log(` Host : ${api.host} ${dim("(localhost only)")}`);
16981
+ console.log(` Port : ${currentPort}`);
16982
+ console.log(` Host : ${currentHost} ${dim("(localhost only)")}`);
16843
16983
  console.log("");
16844
16984
  const newPort = await input({
16845
16985
  message: "API port:",
16846
- default: String(api.port),
16986
+ default: String(currentPort),
16847
16987
  validate: (v) => {
16848
16988
  const n = Number(v.trim());
16849
16989
  if (!Number.isInteger(n) || n < 1 || n > 65535) return "Must be a valid port (1-65535)";
@@ -16852,24 +16992,24 @@ async function editApi(config, updates, settingsManager) {
16852
16992
  });
16853
16993
  if (settingsManager) {
16854
16994
  await settingsManager.updatePluginSettings("@openacp/api-server", { port: Number(newPort.trim()) });
16855
- } else {
16856
- updates.api = { port: Number(newPort.trim()) };
16857
16995
  }
16858
16996
  console.log(ok(`API port set to ${newPort.trim()}`));
16859
16997
  }
16860
16998
  async function editTunnel(config, updates, settingsManager) {
16861
- let tunnel = config.tunnel ?? { enabled: false, port: 3100, provider: "cloudflare", options: {}, storeTtlMinutes: 60, auth: { enabled: false } };
16862
- if (settingsManager) {
16863
- const ps = await settingsManager.loadSettings("@openacp/tunnel");
16864
- if (Object.keys(ps).length > 0) {
16865
- tunnel = { ...tunnel, ...ps };
16866
- }
16867
- }
16868
- const currentUpdates = updates.tunnel ?? {};
16869
- const getVal = (key, fallback) => key in currentUpdates ? currentUpdates[key] : tunnel[key] ?? fallback;
16999
+ const ps = settingsManager ? await settingsManager.loadSettings("@openacp/tunnel") : {};
17000
+ const tunnel = {
17001
+ enabled: ps.enabled ?? false,
17002
+ port: ps.port ?? 3100,
17003
+ provider: ps.provider ?? "openacp",
17004
+ options: ps.options ?? {},
17005
+ storeTtlMinutes: ps.storeTtlMinutes ?? 60,
17006
+ auth: ps.auth ?? { enabled: false }
17007
+ };
17008
+ const tun = { ...tunnel };
17009
+ const getVal = (key, fallback) => key in tun ? tun[key] : tunnel[key] ?? fallback;
16870
17010
  console.log(header("Tunnel"));
16871
17011
  console.log(` Enabled : ${getVal("enabled", false) ? ok("yes") : dim("no")}`);
16872
- console.log(` Provider : ${c.bold}${getVal("provider", "cloudflare")}${c.reset}`);
17012
+ console.log(` Provider : ${getVal("provider", "openacp")}`);
16873
17013
  console.log(` Port : ${getVal("port", 3100)}`);
16874
17014
  const authEnabled = getVal("auth", { enabled: false }).enabled;
16875
17015
  console.log(` Auth : ${authEnabled ? ok("enabled") : dim("disabled")}`);
@@ -16887,8 +17027,6 @@ async function editTunnel(config, updates, settingsManager) {
16887
17027
  ]
16888
17028
  });
16889
17029
  if (choice === "back") break;
16890
- if (!updates.tunnel) updates.tunnel = { ...tunnel };
16891
- const tun = updates.tunnel;
16892
17030
  if (choice === "toggle") {
16893
17031
  const current = getVal("enabled", false);
16894
17032
  if (settingsManager) {
@@ -16901,7 +17039,8 @@ async function editTunnel(config, updates, settingsManager) {
16901
17039
  const provider = await select3({
16902
17040
  message: "Select tunnel provider:",
16903
17041
  choices: [
16904
- { name: "Cloudflare (default)", value: "cloudflare" },
17042
+ { name: "OpenACP (managed)", value: "openacp" },
17043
+ { name: "Cloudflare", value: "cloudflare" },
16905
17044
  { name: "ngrok", value: "ngrok" },
16906
17045
  { name: "bore", value: "bore" },
16907
17046
  { name: "Tailscale Funnel", value: "tailscale" }
@@ -16931,7 +17070,7 @@ async function editTunnel(config, updates, settingsManager) {
16931
17070
  console.log(ok(`Tunnel port set to ${val.trim()}`));
16932
17071
  }
16933
17072
  if (choice === "options") {
16934
- const provider = getVal("provider", "cloudflare");
17073
+ const provider = getVal("provider", "openacp");
16935
17074
  const currentOptions = getVal("options", {});
16936
17075
  await editProviderOptions(provider, currentOptions, tun);
16937
17076
  if (settingsManager) {
@@ -16941,20 +17080,21 @@ async function editTunnel(config, updates, settingsManager) {
16941
17080
  if (choice === "auth") {
16942
17081
  const currentAuth = getVal("auth", { enabled: false });
16943
17082
  if (currentAuth.enabled) {
16944
- tun.auth = { enabled: false };
16945
17083
  if (settingsManager) {
16946
17084
  await settingsManager.updatePluginSettings("@openacp/tunnel", { auth: { enabled: false } });
16947
17085
  }
17086
+ tun.auth = { enabled: false };
16948
17087
  console.log(ok("Tunnel auth disabled"));
16949
17088
  } else {
16950
17089
  const token = await input({
16951
17090
  message: "Auth token (leave empty to auto-generate):",
16952
17091
  default: ""
16953
17092
  });
16954
- tun.auth = token.trim() ? { enabled: true, token: token.trim() } : { enabled: true };
17093
+ const newAuth = token.trim() ? { enabled: true, token: token.trim() } : { enabled: true };
16955
17094
  if (settingsManager) {
16956
- await settingsManager.updatePluginSettings("@openacp/tunnel", { auth: tun.auth });
17095
+ await settingsManager.updatePluginSettings("@openacp/tunnel", { auth: newAuth });
16957
17096
  }
17097
+ tun.auth = newAuth;
16958
17098
  console.log(ok("Tunnel auth enabled"));
16959
17099
  }
16960
17100
  }
@@ -17082,17 +17222,17 @@ ${c.cyan}${c.bold}OpenACP Config Editor${c.reset}`);
17082
17222
  async function sendConfigViaApi(port, updates) {
17083
17223
  const { apiCall: call } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
17084
17224
  const paths = flattenToPaths(updates);
17085
- for (const { path: path35, value } of paths) {
17225
+ for (const { path: path34, value } of paths) {
17086
17226
  const res = await call(port, "/api/config", {
17087
17227
  method: "PATCH",
17088
17228
  headers: { "Content-Type": "application/json" },
17089
- body: JSON.stringify({ path: path35, value })
17229
+ body: JSON.stringify({ path: path34, value })
17090
17230
  });
17091
17231
  const data = await res.json();
17092
17232
  if (!res.ok) {
17093
- console.log(warn(`Failed to update ${path35}: ${data.error}`));
17233
+ console.log(warn(`Failed to update ${path34}: ${data.error}`));
17094
17234
  } else if (data.needsRestart) {
17095
- console.log(warn(`${path35} updated \u2014 restart required`));
17235
+ console.log(warn(`${path34} updated \u2014 restart required`));
17096
17236
  }
17097
17237
  }
17098
17238
  }
@@ -17112,30 +17252,30 @@ function flattenToPaths(obj, prefix = "") {
17112
17252
  // src/cli/daemon.ts
17113
17253
  init_config();
17114
17254
  import { spawn as spawn3 } from "child_process";
17115
- import * as fs30 from "fs";
17116
- import * as path30 from "path";
17255
+ import * as fs29 from "fs";
17256
+ import * as path29 from "path";
17117
17257
  import * as os14 from "os";
17118
- var DEFAULT_ROOT2 = path30.join(os14.homedir(), ".openacp");
17258
+ var DEFAULT_ROOT2 = path29.join(os14.homedir(), ".openacp");
17119
17259
  function getPidPath(root) {
17120
17260
  const base = root ?? DEFAULT_ROOT2;
17121
- return path30.join(base, "openacp.pid");
17261
+ return path29.join(base, "openacp.pid");
17122
17262
  }
17123
17263
  function getLogDir(root) {
17124
17264
  const base = root ?? DEFAULT_ROOT2;
17125
- return path30.join(base, "logs");
17265
+ return path29.join(base, "logs");
17126
17266
  }
17127
17267
  function getRunningMarker(root) {
17128
17268
  const base = root ?? DEFAULT_ROOT2;
17129
- return path30.join(base, "running");
17269
+ return path29.join(base, "running");
17130
17270
  }
17131
17271
  function writePidFile(pidPath, pid) {
17132
- const dir = path30.dirname(pidPath);
17133
- fs30.mkdirSync(dir, { recursive: true });
17134
- fs30.writeFileSync(pidPath, String(pid));
17272
+ const dir = path29.dirname(pidPath);
17273
+ fs29.mkdirSync(dir, { recursive: true });
17274
+ fs29.writeFileSync(pidPath, String(pid));
17135
17275
  }
17136
17276
  function readPidFile(pidPath) {
17137
17277
  try {
17138
- const content = fs30.readFileSync(pidPath, "utf-8").trim();
17278
+ const content = fs29.readFileSync(pidPath, "utf-8").trim();
17139
17279
  const pid = parseInt(content, 10);
17140
17280
  return isNaN(pid) ? null : pid;
17141
17281
  } catch {
@@ -17144,7 +17284,7 @@ function readPidFile(pidPath) {
17144
17284
  }
17145
17285
  function removePidFile(pidPath) {
17146
17286
  try {
17147
- fs30.unlinkSync(pidPath);
17287
+ fs29.unlinkSync(pidPath);
17148
17288
  } catch {
17149
17289
  }
17150
17290
  }
@@ -17177,12 +17317,12 @@ function startDaemon(pidPath = getPidPath(), logDir2, instanceRoot) {
17177
17317
  return { error: `Already running (PID ${pid})` };
17178
17318
  }
17179
17319
  const resolvedLogDir = logDir2 ? expandHome3(logDir2) : getLogDir(instanceRoot);
17180
- fs30.mkdirSync(resolvedLogDir, { recursive: true });
17181
- const logFile = path30.join(resolvedLogDir, "openacp.log");
17182
- const cliPath = path30.resolve(process.argv[1]);
17320
+ fs29.mkdirSync(resolvedLogDir, { recursive: true });
17321
+ const logFile = path29.join(resolvedLogDir, "openacp.log");
17322
+ const cliPath = path29.resolve(process.argv[1]);
17183
17323
  const nodePath = process.execPath;
17184
- const out = fs30.openSync(logFile, "a");
17185
- const err = fs30.openSync(logFile, "a");
17324
+ const out = fs29.openSync(logFile, "a");
17325
+ const err = fs29.openSync(logFile, "a");
17186
17326
  const child = spawn3(nodePath, [cliPath, "--daemon-child"], {
17187
17327
  detached: true,
17188
17328
  stdio: ["ignore", out, err],
@@ -17191,8 +17331,8 @@ function startDaemon(pidPath = getPidPath(), logDir2, instanceRoot) {
17191
17331
  ...instanceRoot ? { OPENACP_INSTANCE_ROOT: instanceRoot } : {}
17192
17332
  }
17193
17333
  });
17194
- fs30.closeSync(out);
17195
- fs30.closeSync(err);
17334
+ fs29.closeSync(out);
17335
+ fs29.closeSync(err);
17196
17336
  if (!child.pid) {
17197
17337
  return { error: "Failed to spawn daemon process" };
17198
17338
  }
@@ -17263,12 +17403,12 @@ async function stopDaemon(pidPath = getPidPath(), instanceRoot) {
17263
17403
  }
17264
17404
  function markRunning(root) {
17265
17405
  const marker = getRunningMarker(root);
17266
- fs30.mkdirSync(path30.dirname(marker), { recursive: true });
17267
- fs30.writeFileSync(marker, "");
17406
+ fs29.mkdirSync(path29.dirname(marker), { recursive: true });
17407
+ fs29.writeFileSync(marker, "");
17268
17408
  }
17269
17409
  function clearRunning(root) {
17270
17410
  try {
17271
- fs30.unlinkSync(getRunningMarker(root));
17411
+ fs29.unlinkSync(getRunningMarker(root));
17272
17412
  } catch {
17273
17413
  }
17274
17414
  }
@@ -17493,6 +17633,9 @@ var Draft = class {
17493
17633
  } finally {
17494
17634
  this.firstFlushPending = false;
17495
17635
  }
17636
+ if (this.buffer !== snapshot) {
17637
+ return this.flush();
17638
+ }
17496
17639
  }
17497
17640
  };
17498
17641
  var DraftManager = class {
@@ -17770,8 +17913,8 @@ Configure via \`security.sessionTimeoutMinutes\` in config.
17770
17913
  3. Copy and run it in your terminal \u2014 the session continues there with full conversation history
17771
17914
 
17772
17915
  ### Terminal \u2192 Chat
17773
- 1. First time: run \`openacp integrate claude\` to install the handoff skill (one-time setup)
17774
- 2. In Claude Code, use the /openacp:handoff slash command
17916
+ 1. First time: run \`openacp integrate <agent>\` to install handoff integration (one-time setup)
17917
+ 2. In supported agents (for example Claude Code or OpenCode), use /openacp:handoff
17775
17918
  3. The session appears as a new topic/thread and you can continue chatting there
17776
17919
 
17777
17920
  ### How it works