@openacp/cli 2026.404.2 → 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,27 +2316,27 @@ 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
2329
  }
2593
- const logsDir = config ? expandHome3(config.logging.logDir) : path24.join(dataDir, "logs");
2330
+ const logsDir = config ? expandHome3(config.logging.logDir) : path23.join(dataDir, "logs");
2594
2331
  return {
2595
2332
  config,
2596
2333
  rawConfig,
2597
2334
  configPath,
2598
2335
  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"),
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"),
2603
2340
  logsDir
2604
2341
  };
2605
2342
  }
@@ -2607,113 +2344,24 @@ var init_doctor = __esm({
2607
2344
  }
2608
2345
  });
2609
2346
 
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 })
2672
- }
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
- };
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
- }
2697
- });
2698
-
2699
2347
  // src/core/plugin/plugin-installer.ts
2700
2348
  var plugin_installer_exports = {};
2701
2349
  __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
  }
@@ -3475,6 +3127,7 @@ var init_sse_manager = __esm({
3475
3127
  "session:deleted",
3476
3128
  "agent:event",
3477
3129
  "permission:request",
3130
+ "permission:resolved",
3478
3131
  "message:queued",
3479
3132
  "message:processing"
3480
3133
  ];
@@ -3541,6 +3194,7 @@ data: ${JSON.stringify(data)}
3541
3194
  const sessionEvents = [
3542
3195
  "agent:event",
3543
3196
  "permission:request",
3197
+ "permission:resolved",
3544
3198
  "session:updated",
3545
3199
  "message:queued",
3546
3200
  "message:processing"
@@ -3586,8 +3240,8 @@ data: ${JSON.stringify(data)}
3586
3240
  });
3587
3241
 
3588
3242
  // src/plugins/api-server/static-server.ts
3589
- import * as fs32 from "fs";
3590
- import * as path32 from "path";
3243
+ import * as fs31 from "fs";
3244
+ import * as path31 from "path";
3591
3245
  import { fileURLToPath } from "url";
3592
3246
  var MIME_TYPES, StaticServer;
3593
3247
  var init_static_server = __esm({
@@ -3611,16 +3265,16 @@ var init_static_server = __esm({
3611
3265
  this.uiDir = uiDir;
3612
3266
  if (!this.uiDir) {
3613
3267
  const __filename = fileURLToPath(import.meta.url);
3614
- const candidate = path32.resolve(path32.dirname(__filename), "../../ui/dist");
3615
- 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"))) {
3616
3270
  this.uiDir = candidate;
3617
3271
  }
3618
3272
  if (!this.uiDir) {
3619
- const publishCandidate = path32.resolve(
3620
- path32.dirname(__filename),
3273
+ const publishCandidate = path31.resolve(
3274
+ path31.dirname(__filename),
3621
3275
  "../ui"
3622
3276
  );
3623
- if (fs32.existsSync(path32.join(publishCandidate, "index.html"))) {
3277
+ if (fs31.existsSync(path31.join(publishCandidate, "index.html"))) {
3624
3278
  this.uiDir = publishCandidate;
3625
3279
  }
3626
3280
  }
@@ -3632,23 +3286,23 @@ var init_static_server = __esm({
3632
3286
  serve(req, res) {
3633
3287
  if (!this.uiDir) return false;
3634
3288
  const urlPath = (req.url || "/").split("?")[0];
3635
- const safePath = path32.normalize(urlPath);
3636
- const filePath = path32.join(this.uiDir, safePath);
3637
- 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)
3638
3292
  return false;
3639
3293
  let realFilePath;
3640
3294
  try {
3641
- realFilePath = fs32.realpathSync(filePath);
3295
+ realFilePath = fs31.realpathSync(filePath);
3642
3296
  } catch {
3643
3297
  realFilePath = null;
3644
3298
  }
3645
3299
  if (realFilePath !== null) {
3646
- const realUiDir = fs32.realpathSync(this.uiDir);
3647
- if (!realFilePath.startsWith(realUiDir + path32.sep) && realFilePath !== realUiDir)
3300
+ const realUiDir = fs31.realpathSync(this.uiDir);
3301
+ if (!realFilePath.startsWith(realUiDir + path31.sep) && realFilePath !== realUiDir)
3648
3302
  return false;
3649
3303
  }
3650
- if (realFilePath !== null && fs32.existsSync(realFilePath) && fs32.statSync(realFilePath).isFile()) {
3651
- const ext = path32.extname(filePath);
3304
+ if (realFilePath !== null && fs31.existsSync(realFilePath) && fs31.statSync(realFilePath).isFile()) {
3305
+ const ext = path31.extname(filePath);
3652
3306
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
3653
3307
  const isHashed = /\.[a-zA-Z0-9]{8,}\.(js|css)$/.test(filePath);
3654
3308
  const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "no-cache";
@@ -3656,16 +3310,16 @@ var init_static_server = __esm({
3656
3310
  "Content-Type": contentType,
3657
3311
  "Cache-Control": cacheControl
3658
3312
  });
3659
- fs32.createReadStream(realFilePath).pipe(res);
3313
+ fs31.createReadStream(realFilePath).pipe(res);
3660
3314
  return true;
3661
3315
  }
3662
- const indexPath = path32.join(this.uiDir, "index.html");
3663
- if (fs32.existsSync(indexPath)) {
3316
+ const indexPath = path31.join(this.uiDir, "index.html");
3317
+ if (fs31.existsSync(indexPath)) {
3664
3318
  res.writeHead(200, {
3665
3319
  "Content-Type": "text/html; charset=utf-8",
3666
3320
  "Cache-Control": "no-cache"
3667
3321
  });
3668
- fs32.createReadStream(indexPath).pipe(res);
3322
+ fs31.createReadStream(indexPath).pipe(res);
3669
3323
  return true;
3670
3324
  }
3671
3325
  return false;
@@ -3820,8 +3474,8 @@ var init_exports = __esm({
3820
3474
  });
3821
3475
 
3822
3476
  // src/plugins/context/context-cache.ts
3823
- import * as fs33 from "fs";
3824
- import * as path33 from "path";
3477
+ import * as fs32 from "fs";
3478
+ import * as path32 from "path";
3825
3479
  import * as crypto2 from "crypto";
3826
3480
  var DEFAULT_TTL_MS, ContextCache;
3827
3481
  var init_context_cache = __esm({
@@ -3832,29 +3486,29 @@ var init_context_cache = __esm({
3832
3486
  constructor(cacheDir, ttlMs = DEFAULT_TTL_MS) {
3833
3487
  this.cacheDir = cacheDir;
3834
3488
  this.ttlMs = ttlMs;
3835
- fs33.mkdirSync(cacheDir, { recursive: true });
3489
+ fs32.mkdirSync(cacheDir, { recursive: true });
3836
3490
  }
3837
3491
  keyHash(repoPath, queryKey) {
3838
3492
  return crypto2.createHash("sha256").update(`${repoPath}:${queryKey}`).digest("hex").slice(0, 16);
3839
3493
  }
3840
3494
  filePath(repoPath, queryKey) {
3841
- return path33.join(this.cacheDir, `${this.keyHash(repoPath, queryKey)}.json`);
3495
+ return path32.join(this.cacheDir, `${this.keyHash(repoPath, queryKey)}.json`);
3842
3496
  }
3843
3497
  get(repoPath, queryKey) {
3844
3498
  const fp = this.filePath(repoPath, queryKey);
3845
3499
  try {
3846
- const stat = fs33.statSync(fp);
3500
+ const stat = fs32.statSync(fp);
3847
3501
  if (Date.now() - stat.mtimeMs > this.ttlMs) {
3848
- fs33.unlinkSync(fp);
3502
+ fs32.unlinkSync(fp);
3849
3503
  return null;
3850
3504
  }
3851
- return JSON.parse(fs33.readFileSync(fp, "utf-8"));
3505
+ return JSON.parse(fs32.readFileSync(fp, "utf-8"));
3852
3506
  } catch {
3853
3507
  return null;
3854
3508
  }
3855
3509
  }
3856
3510
  set(repoPath, queryKey, result) {
3857
- fs33.writeFileSync(this.filePath(repoPath, queryKey), JSON.stringify(result));
3511
+ fs32.writeFileSync(this.filePath(repoPath, queryKey), JSON.stringify(result));
3858
3512
  }
3859
3513
  };
3860
3514
  }
@@ -3862,7 +3516,7 @@ var init_context_cache = __esm({
3862
3516
 
3863
3517
  // src/plugins/context/context-manager.ts
3864
3518
  import * as os15 from "os";
3865
- import * as path34 from "path";
3519
+ import * as path33 from "path";
3866
3520
  var ContextManager;
3867
3521
  var init_context_manager = __esm({
3868
3522
  "src/plugins/context/context-manager.ts"() {
@@ -3872,12 +3526,25 @@ var init_context_manager = __esm({
3872
3526
  providers = [];
3873
3527
  cache;
3874
3528
  historyStore;
3529
+ sessionFlusher;
3875
3530
  constructor(cachePath) {
3876
- 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"));
3877
3532
  }
3878
3533
  setHistoryStore(store) {
3879
3534
  this.historyStore = store;
3880
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
+ }
3881
3548
  async getHistory(sessionId) {
3882
3549
  if (!this.historyStore) return null;
3883
3550
  return this.historyStore.read(sessionId);
@@ -3901,13 +3568,17 @@ var init_context_manager = __esm({
3901
3568
  }
3902
3569
  async buildContext(query, options) {
3903
3570
  const queryKey = `${query.type}:${query.value}:${options?.limit ?? ""}:${options?.maxTokens ?? ""}:${options?.labelAgent ?? ""}`;
3904
- const cached = this.cache.get(query.repoPath, queryKey);
3905
- if (cached) return cached;
3571
+ if (!options?.noCache) {
3572
+ const cached = this.cache.get(query.repoPath, queryKey);
3573
+ if (cached) return cached;
3574
+ }
3906
3575
  for (const provider of this.providers) {
3907
3576
  if (!await provider.isAvailable(query.repoPath)) continue;
3908
3577
  const result = await provider.buildContext(query, options);
3909
3578
  if (result && result.markdown) {
3910
- this.cache.set(query.repoPath, queryKey, result);
3579
+ if (!options?.noCache) {
3580
+ this.cache.set(query.repoPath, queryKey, result);
3581
+ }
3911
3582
  return result;
3912
3583
  }
3913
3584
  }
@@ -3928,7 +3599,7 @@ var init_context_provider = __esm({
3928
3599
  });
3929
3600
 
3930
3601
  // src/plugins/context/entire/checkpoint-reader.ts
3931
- import { execFileSync as execFileSync6 } from "child_process";
3602
+ import { execFileSync as execFileSync7 } from "child_process";
3932
3603
  var ENTIRE_BRANCH, CHECKPOINT_ID_RE, SESSION_ID_RE, CheckpointReader;
3933
3604
  var init_checkpoint_reader = __esm({
3934
3605
  "src/plugins/context/entire/checkpoint-reader.ts"() {
@@ -3947,7 +3618,7 @@ var init_checkpoint_reader = __esm({
3947
3618
  */
3948
3619
  git(...args) {
3949
3620
  try {
3950
- return execFileSync6("git", ["-C", this.repoPath, ...args], {
3621
+ return execFileSync7("git", ["-C", this.repoPath, ...args], {
3951
3622
  encoding: "utf-8"
3952
3623
  }).trim();
3953
3624
  } catch {
@@ -4774,8 +4445,8 @@ function formatToolSummary(name, rawInput, displaySummary) {
4774
4445
  }
4775
4446
  if (lowerName === "grep") {
4776
4447
  const pattern = args.pattern ?? "";
4777
- const path35 = args.path ?? "";
4778
- 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}`;
4779
4450
  }
4780
4451
  if (lowerName === "glob") {
4781
4452
  const pattern = args.pattern ?? "";
@@ -4811,8 +4482,8 @@ function formatToolTitle(name, rawInput, displayTitle) {
4811
4482
  }
4812
4483
  if (lowerName === "grep") {
4813
4484
  const pattern = args.pattern ?? "";
4814
- const path35 = args.path ?? "";
4815
- return pattern ? `"${pattern}"${path35 ? ` in ${path35}` : ""}` : name;
4485
+ const path34 = args.path ?? "";
4486
+ return pattern ? `"${pattern}"${path34 ? ` in ${path34}` : ""}` : name;
4816
4487
  }
4817
4488
  if (lowerName === "glob") {
4818
4489
  return String(args.pattern ?? name);
@@ -5490,12 +5161,34 @@ function asRecord(value) {
5490
5161
  function capitalize(s) {
5491
5162
  return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);
5492
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
+ }
5493
5185
  function buildTitle(entry, kind) {
5494
5186
  if (entry.displayTitle) return entry.displayTitle;
5495
5187
  if (entry.displaySummary) return entry.displaySummary;
5496
5188
  const input2 = asRecord(entry.rawInput);
5189
+ const nameLower = entry.name.toLowerCase();
5497
5190
  if (kind === "read") {
5498
- const filePath = typeof input2.file_path === "string" ? input2.file_path : null;
5191
+ const filePath = getStringField(input2, ["file_path", "filePath", "path"]);
5499
5192
  if (filePath) {
5500
5193
  const startLine = typeof input2.start_line === "number" ? input2.start_line : null;
5501
5194
  const endLine = typeof input2.end_line === "number" ? input2.end_line : null;
@@ -5510,7 +5203,7 @@ function buildTitle(entry, kind) {
5510
5203
  return capitalize(entry.name);
5511
5204
  }
5512
5205
  if (kind === "edit" || kind === "write" || kind === "delete") {
5513
- 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"]);
5514
5207
  if (filePath) return filePath;
5515
5208
  return capitalize(entry.name);
5516
5209
  }
@@ -5542,6 +5235,36 @@ function buildTitle(entry, kind) {
5542
5235
  }
5543
5236
  return capitalize(entry.name);
5544
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
+ }
5545
5268
  if (kind === "fetch" || kind === "web") {
5546
5269
  const url = typeof input2.url === "string" ? input2.url : null;
5547
5270
  if (url && url !== "undefined") return url.length > 60 ? url.slice(0, 57) + "..." : url;
@@ -5549,11 +5272,36 @@ function buildTitle(entry, kind) {
5549
5272
  if (query && query !== "undefined") return query.length > 60 ? query.slice(0, 57) + "..." : query;
5550
5273
  return capitalize(entry.name);
5551
5274
  }
5552
- if (entry.name.toLowerCase() === "skill" && typeof input2.skill === "string" && input2.skill) {
5275
+ if (nameLower === "skill" && typeof input2.skill === "string" && input2.skill) {
5553
5276
  return input2.skill;
5554
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
+ }
5555
5300
  return entry.name;
5556
5301
  }
5302
+ function isRecord(value) {
5303
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5304
+ }
5557
5305
  function buildOutputSummary(content) {
5558
5306
  const lines = content.split("\n").length;
5559
5307
  return `${lines} line${lines === 1 ? "" : "s"} of output`;
@@ -5628,6 +5376,7 @@ var init_display_spec_builder = __esm({
5628
5376
  viewerLinks: entry.viewerLinks,
5629
5377
  outputViewerLink,
5630
5378
  outputFallbackContent,
5379
+ workingDirectory: sessionContext?.workingDirectory,
5631
5380
  status: entry.status,
5632
5381
  isNoise: entry.isNoise,
5633
5382
  isHidden
@@ -5858,20 +5607,33 @@ function renderToolCard(snap) {
5858
5607
  }
5859
5608
  return sections.join("\n\n");
5860
5609
  }
5861
- function shortenTitle(title, kind) {
5862
- 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;
5863
5615
  const parenIdx = title.indexOf(" (");
5864
5616
  const pathPart = parenIdx > 0 ? title.slice(0, parenIdx) : title;
5865
5617
  const rangePart = parenIdx > 0 ? title.slice(parenIdx) : "";
5866
- const fileName = pathPart.split("/").pop() || pathPart;
5867
- 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;
5868
5630
  }
5869
5631
  function renderSpecSection(spec) {
5870
5632
  const lines = [];
5871
5633
  const DONE = /* @__PURE__ */ new Set(["completed", "done", "failed", "error"]);
5872
5634
  const statusPrefix = spec.status === "error" || spec.status === "failed" ? "\u274C " : DONE.has(spec.status) ? "\u2705 " : "\u{1F504} ";
5873
5635
  const kindLabel = KIND_LABELS[spec.kind];
5874
- const displayTitle = shortenTitle(spec.title, spec.kind);
5636
+ const displayTitle = shortenTitle(spec.title, spec.kind, spec.workingDirectory);
5875
5637
  const hasUniqueTitle = displayTitle && displayTitle.toLowerCase() !== kindLabel?.toLowerCase() && displayTitle.toLowerCase() !== spec.kind;
5876
5638
  let titleLine;
5877
5639
  if (kindLabel) {
@@ -5900,9 +5662,9 @@ function renderSpecSection(spec) {
5900
5662
  }
5901
5663
  if (spec.viewerLinks?.file || spec.viewerLinks?.diff || spec.outputViewerLink) {
5902
5664
  const linkParts = [];
5903
- const shortName = displayTitle || kindLabel || spec.kind;
5665
+ const linkName = basename(displayTitle || kindLabel || spec.kind);
5904
5666
  if (spec.viewerLinks?.file)
5905
- 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>`);
5906
5668
  if (spec.viewerLinks?.diff)
5907
5669
  linkParts.push(`<a href="${escapeHtml(spec.viewerLinks.diff)}">View diff</a>`);
5908
5670
  if (spec.outputViewerLink)
@@ -5953,13 +5715,13 @@ __export(version_exports, {
5953
5715
  runUpdate: () => runUpdate
5954
5716
  });
5955
5717
  import { fileURLToPath as fileURLToPath2 } from "url";
5956
- import { dirname as dirname11, join as join21, resolve as resolve5 } from "path";
5957
- 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";
5958
5720
  function findPackageJson() {
5959
- let dir = dirname11(fileURLToPath2(import.meta.url));
5721
+ let dir = dirname10(fileURLToPath2(import.meta.url));
5960
5722
  for (let i = 0; i < 5; i++) {
5961
- const candidate = join21(dir, "package.json");
5962
- if (existsSync18(candidate)) return candidate;
5723
+ const candidate = join20(dir, "package.json");
5724
+ if (existsSync17(candidate)) return candidate;
5963
5725
  const parent = resolve5(dir, "..");
5964
5726
  if (parent === dir) break;
5965
5727
  dir = parent;
@@ -5970,7 +5732,7 @@ function getCurrentVersion() {
5970
5732
  try {
5971
5733
  const pkgPath = findPackageJson();
5972
5734
  if (!pkgPath) return "0.0.0-dev";
5973
- const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
5735
+ const pkg = JSON.parse(readFileSync15(pkgPath, "utf-8"));
5974
5736
  return pkg.version;
5975
5737
  } catch {
5976
5738
  return "0.0.0-dev";
@@ -6347,11 +6109,16 @@ async function createSessionDirect(ctx, core, chatId, agentName, workspace, onCo
6347
6109
  }
6348
6110
  function cacheWorkspace(agentKey, workspace) {
6349
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
+ }
6350
6117
  if (workspaceCache.size > WS_CACHE_MAX) {
6351
- for (const [id2, entry] of workspaceCache) {
6352
- if (now - entry.ts > 5 * 6e4 || workspaceCache.size > WS_CACHE_MAX) {
6353
- workspaceCache.delete(id2);
6354
- }
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);
6355
6122
  }
6356
6123
  }
6357
6124
  const id = nextWsId++;
@@ -6793,9 +6560,12 @@ __export(integrate_exports, {
6793
6560
  listIntegrations: () => listIntegrations,
6794
6561
  uninstallIntegration: () => uninstallIntegration
6795
6562
  });
6796
- import { existsSync as existsSync19, mkdirSync as mkdirSync12, readFileSync as readFileSync15, writeFileSync as writeFileSync12, unlinkSync as unlinkSync7, chmodSync, rmdirSync } from "fs";
6797
- 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";
6798
6565
  import { homedir as homedir10 } from "os";
6566
+ function isHooksIntegrationSpec(spec) {
6567
+ return spec.strategy === "hooks";
6568
+ }
6799
6569
  function expandPath(p) {
6800
6570
  return p.replace(/^~/, homedir10());
6801
6571
  }
@@ -6913,12 +6683,48 @@ Examples:
6913
6683
  /openacp:handoff telegram
6914
6684
  `;
6915
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
+ }
6916
6722
  function mergeSettingsJson(settingsPath, hookEvent, hookScriptPath) {
6917
6723
  const fullPath = expandPath(settingsPath);
6918
6724
  let settings = {};
6919
- if (existsSync19(fullPath)) {
6920
- const raw = readFileSync15(fullPath, "utf-8");
6921
- writeFileSync12(`${fullPath}.bak`, raw);
6725
+ if (existsSync18(fullPath)) {
6726
+ const raw = readFileSync16(fullPath, "utf-8");
6727
+ writeFileSync11(`${fullPath}.bak`, raw);
6922
6728
  settings = JSON.parse(raw);
6923
6729
  }
6924
6730
  const hooks = settings.hooks ?? {};
@@ -6933,15 +6739,15 @@ function mergeSettingsJson(settingsPath, hookEvent, hookScriptPath) {
6933
6739
  hooks: [{ type: "command", command: hookScriptPath }]
6934
6740
  });
6935
6741
  }
6936
- mkdirSync12(dirname12(fullPath), { recursive: true });
6937
- writeFileSync12(fullPath, JSON.stringify(settings, null, 2) + "\n");
6742
+ mkdirSync11(dirname11(fullPath), { recursive: true });
6743
+ writeFileSync11(fullPath, JSON.stringify(settings, null, 2) + "\n");
6938
6744
  }
6939
6745
  function mergeHooksJson(settingsPath, hookEvent, hookScriptPath) {
6940
6746
  const fullPath = expandPath(settingsPath);
6941
6747
  let config = { version: 1 };
6942
- if (existsSync19(fullPath)) {
6943
- const raw = readFileSync15(fullPath, "utf-8");
6944
- writeFileSync12(`${fullPath}.bak`, raw);
6748
+ if (existsSync18(fullPath)) {
6749
+ const raw = readFileSync16(fullPath, "utf-8");
6750
+ writeFileSync11(`${fullPath}.bak`, raw);
6945
6751
  config = JSON.parse(raw);
6946
6752
  }
6947
6753
  const hooks = config.hooks ?? {};
@@ -6952,13 +6758,13 @@ function mergeHooksJson(settingsPath, hookEvent, hookScriptPath) {
6952
6758
  if (!alreadyInstalled) {
6953
6759
  eventHooks.push({ command: hookScriptPath });
6954
6760
  }
6955
- mkdirSync12(dirname12(fullPath), { recursive: true });
6956
- writeFileSync12(fullPath, JSON.stringify(config, null, 2) + "\n");
6761
+ mkdirSync11(dirname11(fullPath), { recursive: true });
6762
+ writeFileSync11(fullPath, JSON.stringify(config, null, 2) + "\n");
6957
6763
  }
6958
6764
  function removeFromSettingsJson(settingsPath, hookEvent) {
6959
6765
  const fullPath = expandPath(settingsPath);
6960
- if (!existsSync19(fullPath)) return;
6961
- const raw = readFileSync15(fullPath, "utf-8");
6766
+ if (!existsSync18(fullPath)) return;
6767
+ const raw = readFileSync16(fullPath, "utf-8");
6962
6768
  const settings = JSON.parse(raw);
6963
6769
  const hooks = settings.hooks;
6964
6770
  if (!hooks?.[hookEvent]) return;
@@ -6968,12 +6774,12 @@ function removeFromSettingsJson(settingsPath, hookEvent) {
6968
6774
  if (hooks[hookEvent].length === 0) {
6969
6775
  delete hooks[hookEvent];
6970
6776
  }
6971
- writeFileSync12(fullPath, JSON.stringify(settings, null, 2) + "\n");
6777
+ writeFileSync11(fullPath, JSON.stringify(settings, null, 2) + "\n");
6972
6778
  }
6973
6779
  function removeFromHooksJson(settingsPath, hookEvent) {
6974
6780
  const fullPath = expandPath(settingsPath);
6975
- if (!existsSync19(fullPath)) return;
6976
- const raw = readFileSync15(fullPath, "utf-8");
6781
+ if (!existsSync18(fullPath)) return;
6782
+ const raw = readFileSync16(fullPath, "utf-8");
6977
6783
  const config = JSON.parse(raw);
6978
6784
  const hooks = config.hooks;
6979
6785
  if (!hooks?.[hookEvent]) return;
@@ -6983,9 +6789,9 @@ function removeFromHooksJson(settingsPath, hookEvent) {
6983
6789
  if (hooks[hookEvent].length === 0) {
6984
6790
  delete hooks[hookEvent];
6985
6791
  }
6986
- writeFileSync12(fullPath, JSON.stringify(config, null, 2) + "\n");
6792
+ writeFileSync11(fullPath, JSON.stringify(config, null, 2) + "\n");
6987
6793
  }
6988
- async function installIntegration(agentKey, spec) {
6794
+ async function installHooksIntegration(agentKey, spec) {
6989
6795
  const logs = [];
6990
6796
  try {
6991
6797
  if (!commandExists("jq")) {
@@ -6995,31 +6801,31 @@ async function installIntegration(agentKey, spec) {
6995
6801
  };
6996
6802
  }
6997
6803
  const hooksDir = expandPath(spec.hooksDirPath);
6998
- mkdirSync12(hooksDir, { recursive: true });
6999
- const injectPath = join22(hooksDir, "openacp-inject-session.sh");
7000
- 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));
7001
6807
  chmodSync(injectPath, 493);
7002
6808
  logs.push(`Created ${injectPath}`);
7003
- const handoffPath = join22(hooksDir, "openacp-handoff.sh");
7004
- writeFileSync12(handoffPath, generateHandoffScript(agentKey));
6809
+ const handoffPath = join21(hooksDir, "openacp-handoff.sh");
6810
+ writeFileSync11(handoffPath, generateHandoffScript(agentKey));
7005
6811
  chmodSync(handoffPath, 493);
7006
6812
  logs.push(`Created ${handoffPath}`);
7007
6813
  if (spec.commandsPath && spec.handoffCommandName) {
7008
6814
  if (spec.commandFormat === "skill") {
7009
- const skillDir = expandPath(join22(spec.commandsPath, spec.handoffCommandName));
7010
- mkdirSync12(skillDir, { recursive: true });
7011
- const skillPath = join22(skillDir, "SKILL.md");
7012
- 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));
7013
6819
  logs.push(`Created ${skillPath}`);
7014
6820
  } else {
7015
6821
  const cmdsDir = expandPath(spec.commandsPath);
7016
- mkdirSync12(cmdsDir, { recursive: true });
7017
- const cmdPath = join22(cmdsDir, `${spec.handoffCommandName}.md`);
7018
- writeFileSync12(cmdPath, generateHandoffCommand(agentKey, spec));
6822
+ mkdirSync11(cmdsDir, { recursive: true });
6823
+ const cmdPath = join21(cmdsDir, `${spec.handoffCommandName}.md`);
6824
+ writeFileSync11(cmdPath, generateHandoffCommand(agentKey, spec));
7019
6825
  logs.push(`Created ${cmdPath}`);
7020
6826
  }
7021
6827
  }
7022
- const injectFullPath = join22(hooksDir, "openacp-inject-session.sh");
6828
+ const injectFullPath = join21(hooksDir, "openacp-inject-session.sh");
7023
6829
  if (spec.settingsFormat === "hooks_json") {
7024
6830
  mergeHooksJson(spec.settingsPath, spec.hookEvent, injectFullPath);
7025
6831
  } else {
@@ -7032,22 +6838,22 @@ async function installIntegration(agentKey, spec) {
7032
6838
  return { success: false, logs };
7033
6839
  }
7034
6840
  }
7035
- async function uninstallIntegration(agentKey, spec) {
6841
+ async function uninstallHooksIntegration(agentKey, spec) {
7036
6842
  const logs = [];
7037
6843
  try {
7038
6844
  const hooksDir = expandPath(spec.hooksDirPath);
7039
6845
  for (const filename of ["openacp-inject-session.sh", "openacp-handoff.sh"]) {
7040
- const filePath = join22(hooksDir, filename);
7041
- if (existsSync19(filePath)) {
6846
+ const filePath = join21(hooksDir, filename);
6847
+ if (existsSync18(filePath)) {
7042
6848
  unlinkSync7(filePath);
7043
6849
  logs.push(`Removed ${filePath}`);
7044
6850
  }
7045
6851
  }
7046
6852
  if (spec.commandsPath && spec.handoffCommandName) {
7047
6853
  if (spec.commandFormat === "skill") {
7048
- const skillDir = expandPath(join22(spec.commandsPath, spec.handoffCommandName));
7049
- const skillPath = join22(skillDir, "SKILL.md");
7050
- if (existsSync19(skillPath)) {
6854
+ const skillDir = expandPath(join21(spec.commandsPath, spec.handoffCommandName));
6855
+ const skillPath = join21(skillDir, "SKILL.md");
6856
+ if (existsSync18(skillPath)) {
7051
6857
  unlinkSync7(skillPath);
7052
6858
  try {
7053
6859
  rmdirSync(skillDir);
@@ -7056,8 +6862,8 @@ async function uninstallIntegration(agentKey, spec) {
7056
6862
  logs.push(`Removed ${skillPath}`);
7057
6863
  }
7058
6864
  } else {
7059
- const cmdPath = expandPath(join22(spec.commandsPath, `${spec.handoffCommandName}.md`));
7060
- if (existsSync19(cmdPath)) {
6865
+ const cmdPath = expandPath(join21(spec.commandsPath, `${spec.handoffCommandName}.md`));
6866
+ if (existsSync18(cmdPath)) {
7061
6867
  unlinkSync7(cmdPath);
7062
6868
  logs.push(`Removed ${cmdPath}`);
7063
6869
  }
@@ -7075,14 +6881,82 @@ async function uninstallIntegration(agentKey, spec) {
7075
6881
  return { success: false, logs };
7076
6882
  }
7077
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
+ }
7078
6947
  function buildHandoffItem(agentKey, spec) {
7079
- const hooksDir = expandPath(spec.hooksDirPath);
7080
6948
  return {
7081
6949
  id: "handoff",
7082
6950
  name: "Handoff",
7083
6951
  description: "Transfer sessions between terminal and messaging platforms",
7084
6952
  isInstalled() {
7085
- 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);
7086
6960
  },
7087
6961
  install: () => installIntegration(agentKey, spec),
7088
6962
  uninstall: () => uninstallIntegration(agentKey, spec)
@@ -7094,23 +6968,24 @@ function getSkillBasePath(spec) {
7094
6968
  return expandPath(skillsBase);
7095
6969
  }
7096
6970
  function buildTunnelItem(spec) {
7097
- if (!spec.commandsPath) return null;
6971
+ if (!isHooksIntegrationSpec(spec) || !spec.commandsPath) return null;
6972
+ const hooksSpec = spec;
7098
6973
  function getTunnelPath() {
7099
- return join22(getSkillBasePath(spec), "openacp-tunnel", "SKILL.md");
6974
+ return join21(getSkillBasePath(hooksSpec), "openacp-tunnel", "SKILL.md");
7100
6975
  }
7101
6976
  return {
7102
6977
  id: "tunnel",
7103
6978
  name: "Tunnel",
7104
6979
  description: "Expose local ports to the internet via OpenACP tunnel",
7105
6980
  isInstalled() {
7106
- return existsSync19(getTunnelPath());
6981
+ return existsSync18(getTunnelPath());
7107
6982
  },
7108
6983
  async install() {
7109
6984
  const logs = [];
7110
6985
  try {
7111
6986
  const skillPath = getTunnelPath();
7112
- mkdirSync12(dirname12(skillPath), { recursive: true });
7113
- writeFileSync12(skillPath, generateTunnelCommand());
6987
+ mkdirSync11(dirname11(skillPath), { recursive: true });
6988
+ writeFileSync11(skillPath, generateTunnelCommand());
7114
6989
  logs.push(`Created ${skillPath}`);
7115
6990
  return { success: true, logs };
7116
6991
  } catch (err) {
@@ -7122,10 +6997,10 @@ function buildTunnelItem(spec) {
7122
6997
  const logs = [];
7123
6998
  try {
7124
6999
  const skillPath = getTunnelPath();
7125
- if (existsSync19(skillPath)) {
7000
+ if (existsSync18(skillPath)) {
7126
7001
  unlinkSync7(skillPath);
7127
7002
  try {
7128
- rmdirSync(dirname12(skillPath));
7003
+ rmdirSync(dirname11(skillPath));
7129
7004
  } catch {
7130
7005
  }
7131
7006
  logs.push(`Removed ${skillPath}`);
@@ -7561,7 +7436,7 @@ async function buildSettingsKeyboard(core) {
7561
7436
  const fields = getSafeFields();
7562
7437
  const kb = new InlineKeyboard6();
7563
7438
  for (const field of fields) {
7564
- const value = await getFieldValueAsync(field, core.configManager, core.settingsManager);
7439
+ const value = getConfigValue(core.configManager.get(), field.path);
7565
7440
  const label = formatFieldLabel(field, value);
7566
7441
  if (field.type === "toggle") {
7567
7442
  kb.text(`${label}`, `s:toggle:${field.path}`).row();
@@ -7604,11 +7479,10 @@ function setupSettingsCallbacks(bot, core, getAssistantSession) {
7604
7479
  const fieldPath = ctx.callbackQuery.data.replace("s:toggle:", "");
7605
7480
  const fieldDef = getSafeFields().find((f) => f.path === fieldPath);
7606
7481
  if (!fieldDef) return;
7607
- const settingsManager = core.settingsManager;
7608
- const currentValue = await getFieldValueAsync(fieldDef, core.configManager, settingsManager);
7482
+ const currentValue = getConfigValue(core.configManager.get(), fieldDef.path);
7609
7483
  const newValue = !currentValue;
7610
7484
  try {
7611
- await setFieldValueAsync(fieldDef, newValue, core.configManager, settingsManager);
7485
+ await setFieldValueAsync(fieldDef, newValue, core.configManager);
7612
7486
  const toast = isHotReloadable(fieldPath) ? `\u2705 ${fieldPath} = ${newValue}` : `\u2705 ${fieldPath} = ${newValue} (restart needed)`;
7613
7487
  try {
7614
7488
  await ctx.answerCallbackQuery({ text: toast });
@@ -7632,7 +7506,7 @@ function setupSettingsCallbacks(bot, core, getAssistantSession) {
7632
7506
  const fieldDef = getSafeFields().find((f) => f.path === fieldPath);
7633
7507
  if (!fieldDef) return;
7634
7508
  const options = resolveOptions(fieldDef, config) ?? [];
7635
- const currentValue = await getFieldValueAsync(fieldDef, core.configManager, core.settingsManager);
7509
+ const currentValue = getConfigValue(core.configManager.get(), fieldDef.path);
7636
7510
  const kb = new InlineKeyboard6();
7637
7511
  for (const opt of options) {
7638
7512
  const marker = opt === String(currentValue) ? " \u2713" : "";
@@ -7666,9 +7540,7 @@ Select a value:`, {
7666
7540
  const speechSettings = await sm.loadSettings("@openacp/speech");
7667
7541
  hasApiKey = !!speechSettings.groqApiKey;
7668
7542
  } else {
7669
- const config = core.configManager.get();
7670
- const providerConfig = config.speech?.stt?.providers?.[newValue];
7671
- hasApiKey = !!providerConfig?.apiKey;
7543
+ hasApiKey = false;
7672
7544
  }
7673
7545
  if (!hasApiKey) {
7674
7546
  const assistant = getAssistantSession();
@@ -7688,7 +7560,7 @@ Select a value:`, {
7688
7560
  return;
7689
7561
  }
7690
7562
  }
7691
- await setFieldValueAsync(fieldDef, newValue, core.configManager, core.settingsManager);
7563
+ await setFieldValueAsync(fieldDef, newValue, core.configManager);
7692
7564
  try {
7693
7565
  await ctx.answerCallbackQuery({ text: `\u2705 ${fieldPath} = ${newValue}` });
7694
7566
  } catch {
@@ -7713,7 +7585,7 @@ Tap to change:`, {
7713
7585
  const fieldPath = ctx.callbackQuery.data.replace("s:input:", "");
7714
7586
  const fieldDef = getSafeFields().find((f) => f.path === fieldPath);
7715
7587
  if (!fieldDef) return;
7716
- const currentValue = await getFieldValueAsync(fieldDef, core.configManager, core.settingsManager);
7588
+ const currentValue = getConfigValue(core.configManager.get(), fieldDef.path);
7717
7589
  const assistant = getAssistantSession();
7718
7590
  if (!assistant) {
7719
7591
  try {
@@ -9274,6 +9146,130 @@ var init_renderer2 = __esm({
9274
9146
  }
9275
9147
  });
9276
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
+
9277
9273
  // src/plugins/telegram/adapter.ts
9278
9274
  import { Bot, InputFile } from "grammy";
9279
9275
  function patchedFetch(input2, init) {
@@ -9343,6 +9339,10 @@ var init_adapter = __esm({
9343
9339
  controlMsgIds = /* @__PURE__ */ new Map();
9344
9340
  _threadReadyHandler;
9345
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;
9346
9346
  /** Store control message ID in memory + persist to session record */
9347
9347
  storeControlMsgId(sessionId, msgId) {
9348
9348
  this.controlMsgIds.set(sessionId, msgId);
@@ -9471,25 +9471,6 @@ var init_adapter = __esm({
9471
9471
  if (chatId !== this.telegramConfig.chatId) return;
9472
9472
  return next();
9473
9473
  });
9474
- const topics = await this.retryWithBackoff(
9475
- () => ensureTopics(
9476
- this.bot,
9477
- this.telegramConfig.chatId,
9478
- this.telegramConfig,
9479
- async (updates) => {
9480
- if (this.saveTopicIds) {
9481
- await this.saveTopicIds(updates);
9482
- } else {
9483
- await this.core.configManager.save({
9484
- channels: { telegram: updates }
9485
- });
9486
- }
9487
- }
9488
- ),
9489
- "ensureTopics"
9490
- );
9491
- this.notificationTopicId = topics.notificationTopicId;
9492
- this.assistantTopicId = topics.assistantTopicId;
9493
9474
  this.permissionHandler = new PermissionHandler(
9494
9475
  this.bot,
9495
9476
  this.telegramConfig.chatId,
@@ -9499,6 +9480,13 @@ var init_adapter = __esm({
9499
9480
  this.bot.on("message:text", async (ctx, next) => {
9500
9481
  const text3 = ctx.message?.text;
9501
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
+ }
9502
9490
  const registry = this.core.lifecycleManager?.serviceRegistry?.get(
9503
9491
  "command-registry"
9504
9492
  );
@@ -9556,6 +9544,11 @@ var init_adapter = __esm({
9556
9544
  }
9557
9545
  });
9558
9546
  this.bot.callbackQuery(/^c\//, async (ctx) => {
9547
+ if (!this._topicsInitialized) {
9548
+ await ctx.answerCallbackQuery().catch(() => {
9549
+ });
9550
+ return;
9551
+ }
9559
9552
  const data = ctx.callbackQuery.data;
9560
9553
  const command = this.fromCallbackData(data);
9561
9554
  const registry = this.core.lifecycleManager?.serviceRegistry?.get(
@@ -9612,6 +9605,80 @@ var init_adapter = __esm({
9612
9605
  setupTTSCallbacks(this.bot, this.core);
9613
9606
  setupVerbosityCallbacks(this.bot, this.core);
9614
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;
9615
9682
  setupAllCallbacks(
9616
9683
  this.bot,
9617
9684
  this.core,
@@ -9641,7 +9708,6 @@ ${p}` : p;
9641
9708
  this.storeControlMsgId(sessionId, msgId);
9642
9709
  }
9643
9710
  );
9644
- this.permissionHandler.setupCallbackHandler();
9645
9711
  this._threadReadyHandler = ({ sessionId, channelId, threadId }) => {
9646
9712
  if (channelId !== "telegram") return;
9647
9713
  const session = this.core.sessionManager.getSession(sessionId);
@@ -9679,13 +9745,6 @@ ${p}` : p;
9679
9745
  };
9680
9746
  this.core.eventBus.on("session:configChanged", this._configChangedHandler);
9681
9747
  this.setupRoutes();
9682
- this.bot.start({
9683
- allowed_updates: ["message", "callback_query"],
9684
- onStart: () => log33.info(
9685
- { chatId: this.telegramConfig.chatId },
9686
- "Telegram bot started"
9687
- )
9688
- });
9689
9748
  try {
9690
9749
  const config = this.core.configManager.get();
9691
9750
  const agents = this.core.agentManager.getAvailableAgents();
@@ -9715,42 +9774,56 @@ ${p}` : p;
9715
9774
  } catch (err) {
9716
9775
  log33.error({ err }, "Failed to spawn assistant");
9717
9776
  }
9777
+ this._topicsInitialized = true;
9778
+ log33.info("Telegram adapter fully initialized");
9718
9779
  }
9719
- /**
9720
- * Retry an async operation with exponential backoff.
9721
- * Used for Telegram API calls that may fail due to transient network issues.
9722
- */
9723
- async retryWithBackoff(fn, label, maxRetries = 5, baseDelayMs = 2e3) {
9724
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
9725
- try {
9726
- return await fn();
9727
- } catch (err) {
9728
- if (attempt === maxRetries) throw err;
9729
- const delay = baseDelayMs * Math.pow(2, attempt - 1);
9730
- log33.warn(
9731
- { err, attempt, maxRetries, delayMs: delay, operation: label },
9732
- `${label} failed, retrying in ${delay}ms`
9733
- );
9734
- await new Promise((r) => setTimeout(r, delay));
9735
- }
9736
- }
9737
- throw new Error("unreachable");
9738
- }
9739
- /**
9740
- * Register Telegram commands in the background with retries.
9741
- * Non-critical — bot works fine without autocomplete commands.
9742
- */
9743
- registerCommandsWithRetry() {
9744
- this.retryWithBackoff(
9745
- () => this.bot.api.setMyCommands(STATIC_COMMANDS, {
9746
- scope: { type: "chat", chat_id: this.telegramConfig.chatId }
9747
- }),
9748
- "setMyCommands"
9749
- ).catch((err) => {
9750
- 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");
9751
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]);
9752
9821
  }
9753
9822
  async stop() {
9823
+ if (this._prerequisiteWatcher !== null) {
9824
+ clearTimeout(this._prerequisiteWatcher);
9825
+ this._prerequisiteWatcher = null;
9826
+ }
9754
9827
  for (const tracker of this.sessionTrackers.values()) {
9755
9828
  tracker.destroy();
9756
9829
  }
@@ -10536,8 +10609,16 @@ function nodeToWebWritable(nodeStream) {
10536
10609
  resolve6();
10537
10610
  return;
10538
10611
  }
10539
- nodeStream.once("drain", resolve6);
10540
- 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);
10541
10622
  });
10542
10623
  },
10543
10624
  close() {
@@ -10584,13 +10665,13 @@ init_config();
10584
10665
  // src/core/agents/agent-instance.ts
10585
10666
  import { spawn as spawn2, execFileSync } from "child_process";
10586
10667
  import { Transform } from "stream";
10587
- import fs9 from "fs";
10588
- import path8 from "path";
10668
+ import fs8 from "fs";
10669
+ import path7 from "path";
10589
10670
  import { ClientSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
10590
10671
 
10591
10672
  // src/core/security/path-guard.ts
10592
- import fs6 from "fs";
10593
- import path6 from "path";
10673
+ import fs5 from "fs";
10674
+ import path5 from "path";
10594
10675
  import ignore from "ignore";
10595
10676
  var DEFAULT_DENY_PATTERNS = [
10596
10677
  ".env",
@@ -10610,15 +10691,15 @@ var PathGuard = class {
10610
10691
  ig;
10611
10692
  constructor(options) {
10612
10693
  try {
10613
- this.cwd = fs6.realpathSync(path6.resolve(options.cwd));
10694
+ this.cwd = fs5.realpathSync(path5.resolve(options.cwd));
10614
10695
  } catch {
10615
- this.cwd = path6.resolve(options.cwd);
10696
+ this.cwd = path5.resolve(options.cwd);
10616
10697
  }
10617
10698
  this.allowedPaths = options.allowedPaths.map((p) => {
10618
10699
  try {
10619
- return fs6.realpathSync(path6.resolve(p));
10700
+ return fs5.realpathSync(path5.resolve(p));
10620
10701
  } catch {
10621
- return path6.resolve(p);
10702
+ return path5.resolve(p);
10622
10703
  }
10623
10704
  });
10624
10705
  this.ig = ignore();
@@ -10628,19 +10709,19 @@ var PathGuard = class {
10628
10709
  }
10629
10710
  }
10630
10711
  validatePath(targetPath, operation) {
10631
- const resolved = path6.resolve(targetPath);
10712
+ const resolved = path5.resolve(targetPath);
10632
10713
  let realPath;
10633
10714
  try {
10634
- realPath = fs6.realpathSync(resolved);
10715
+ realPath = fs5.realpathSync(resolved);
10635
10716
  } catch {
10636
10717
  realPath = resolved;
10637
10718
  }
10638
- if (operation === "write" && path6.basename(realPath) === ".openacpignore") {
10719
+ if (operation === "write" && path5.basename(realPath) === ".openacpignore") {
10639
10720
  return { allowed: false, reason: "Cannot write to .openacpignore" };
10640
10721
  }
10641
- const isWithinCwd = realPath === this.cwd || realPath.startsWith(this.cwd + path6.sep);
10722
+ const isWithinCwd = realPath === this.cwd || realPath.startsWith(this.cwd + path5.sep);
10642
10723
  const isWithinAllowed = this.allowedPaths.some(
10643
- (ap) => realPath === ap || realPath.startsWith(ap + path6.sep)
10724
+ (ap) => realPath === ap || realPath.startsWith(ap + path5.sep)
10644
10725
  );
10645
10726
  if (!isWithinCwd && !isWithinAllowed) {
10646
10727
  return {
@@ -10649,7 +10730,7 @@ var PathGuard = class {
10649
10730
  };
10650
10731
  }
10651
10732
  if (isWithinCwd && !isWithinAllowed) {
10652
- const relativePath = path6.relative(this.cwd, realPath);
10733
+ const relativePath = path5.relative(this.cwd, realPath);
10653
10734
  if (relativePath === ".openacpignore") {
10654
10735
  return { allowed: true, reason: "" };
10655
10736
  }
@@ -10664,15 +10745,15 @@ var PathGuard = class {
10664
10745
  }
10665
10746
  addAllowedPath(p) {
10666
10747
  try {
10667
- this.allowedPaths.push(fs6.realpathSync(path6.resolve(p)));
10748
+ this.allowedPaths.push(fs5.realpathSync(path5.resolve(p)));
10668
10749
  } catch {
10669
- this.allowedPaths.push(path6.resolve(p));
10750
+ this.allowedPaths.push(path5.resolve(p));
10670
10751
  }
10671
10752
  }
10672
10753
  static loadIgnoreFile(cwd) {
10673
- const ignorePath = path6.join(cwd, ".openacpignore");
10754
+ const ignorePath = path5.join(cwd, ".openacpignore");
10674
10755
  try {
10675
- const content = fs6.readFileSync(ignorePath, "utf-8");
10756
+ const content = fs5.readFileSync(ignorePath, "utf-8");
10676
10757
  return content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
10677
10758
  } catch {
10678
10759
  return [];
@@ -10727,7 +10808,8 @@ function filterEnv(processEnv, agentEnv, whitelist) {
10727
10808
  }
10728
10809
 
10729
10810
  // src/core/utils/typed-emitter.ts
10730
- var TypedEmitter = class {
10811
+ var TypedEmitter = class _TypedEmitter {
10812
+ static MAX_BUFFER_SIZE = 1e4;
10731
10813
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
10732
10814
  listeners = /* @__PURE__ */ new Map();
10733
10815
  paused = false;
@@ -10751,6 +10833,10 @@ var TypedEmitter = class {
10751
10833
  this.deliver(event, args);
10752
10834
  } else {
10753
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
+ }
10754
10840
  }
10755
10841
  return;
10756
10842
  }
@@ -10795,7 +10881,11 @@ var TypedEmitter = class {
10795
10881
  const set = this.listeners.get(event);
10796
10882
  if (!set) return;
10797
10883
  for (const listener of set) {
10798
- listener(...args);
10884
+ try {
10885
+ listener(...args);
10886
+ } catch (err) {
10887
+ console.error(`[EventBus] Listener error on "${String(event)}":`, err);
10888
+ }
10799
10889
  }
10800
10890
  }
10801
10891
  };
@@ -10886,6 +10976,11 @@ var TerminalManager = class {
10886
10976
  }, async (p) => p).catch(() => {
10887
10977
  });
10888
10978
  }
10979
+ setTimeout(() => {
10980
+ if (this.terminals.has(terminalId)) {
10981
+ this.terminals.delete(terminalId);
10982
+ }
10983
+ }, 3e4).unref();
10889
10984
  });
10890
10985
  return { terminalId };
10891
10986
  }
@@ -10918,6 +11013,12 @@ var TerminalManager = class {
10918
11013
  state.process.on("exit", (code, signal) => {
10919
11014
  resolve6({ exitCode: code, signal });
10920
11015
  });
11016
+ if (state.exitStatus !== null) {
11017
+ resolve6({
11018
+ exitCode: state.exitStatus.exitCode,
11019
+ signal: state.exitStatus.signal
11020
+ });
11021
+ }
10921
11022
  });
10922
11023
  }
10923
11024
  kill(terminalId) {
@@ -10955,24 +11056,24 @@ var McpManager = class {
10955
11056
  };
10956
11057
 
10957
11058
  // src/core/utils/debug-tracer.ts
10958
- import fs8 from "fs";
10959
- import path7 from "path";
11059
+ import fs7 from "fs";
11060
+ import path6 from "path";
10960
11061
  var DEBUG_ENABLED = process.env.OPENACP_DEBUG === "true" || process.env.OPENACP_DEBUG === "1";
10961
11062
  var DebugTracer = class {
10962
11063
  constructor(sessionId, workingDirectory) {
10963
11064
  this.sessionId = sessionId;
10964
11065
  this.workingDirectory = workingDirectory;
10965
- this.logDir = path7.join(workingDirectory, ".log");
11066
+ this.logDir = path6.join(workingDirectory, ".log");
10966
11067
  }
10967
11068
  dirCreated = false;
10968
11069
  logDir;
10969
11070
  log(layer, data) {
10970
11071
  try {
10971
11072
  if (!this.dirCreated) {
10972
- fs8.mkdirSync(this.logDir, { recursive: true });
11073
+ fs7.mkdirSync(this.logDir, { recursive: true });
10973
11074
  this.dirCreated = true;
10974
11075
  }
10975
- const filePath = path7.join(this.logDir, `${this.sessionId}_${layer}.jsonl`);
11076
+ const filePath = path6.join(this.logDir, `${this.sessionId}_${layer}.jsonl`);
10976
11077
  const seen = /* @__PURE__ */ new WeakSet();
10977
11078
  const line = JSON.stringify({ ts: Date.now(), ...data }, (_key, value) => {
10978
11079
  if (typeof value === "object" && value !== null) {
@@ -10981,7 +11082,7 @@ var DebugTracer = class {
10981
11082
  }
10982
11083
  return value;
10983
11084
  }) + "\n";
10984
- fs8.appendFileSync(filePath, line);
11085
+ fs7.appendFileSync(filePath, line);
10985
11086
  } catch {
10986
11087
  }
10987
11088
  }
@@ -10999,11 +11100,11 @@ init_log();
10999
11100
  var log4 = createChildLogger({ module: "agent-instance" });
11000
11101
  function findPackageRoot(startDir) {
11001
11102
  let dir = startDir;
11002
- while (dir !== path8.dirname(dir)) {
11003
- if (fs9.existsSync(path8.join(dir, "package.json"))) {
11103
+ while (dir !== path7.dirname(dir)) {
11104
+ if (fs8.existsSync(path7.join(dir, "package.json"))) {
11004
11105
  return dir;
11005
11106
  }
11006
- dir = path8.dirname(dir);
11107
+ dir = path7.dirname(dir);
11007
11108
  }
11008
11109
  return startDir;
11009
11110
  }
@@ -11015,26 +11116,26 @@ function resolveAgentCommand(cmd) {
11015
11116
  }
11016
11117
  for (const root of searchRoots) {
11017
11118
  const packageDirs = [
11018
- path8.resolve(root, "node_modules", "@zed-industries", cmd, "dist", "index.js"),
11019
- 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")
11020
11121
  ];
11021
11122
  for (const jsPath of packageDirs) {
11022
- if (fs9.existsSync(jsPath)) {
11123
+ if (fs8.existsSync(jsPath)) {
11023
11124
  return { command: process.execPath, args: [jsPath] };
11024
11125
  }
11025
11126
  }
11026
11127
  }
11027
11128
  for (const root of searchRoots) {
11028
- const localBin = path8.resolve(root, "node_modules", ".bin", cmd);
11029
- if (fs9.existsSync(localBin)) {
11030
- 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");
11031
11132
  if (content.startsWith("#!/usr/bin/env node")) {
11032
11133
  return { command: process.execPath, args: [localBin] };
11033
11134
  }
11034
11135
  const match = content.match(/"([^"]+\.js)"/);
11035
11136
  if (match) {
11036
- const target = path8.resolve(path8.dirname(localBin), match[1]);
11037
- if (fs9.existsSync(target)) {
11137
+ const target = path7.resolve(path7.dirname(localBin), match[1]);
11138
+ if (fs8.existsSync(target)) {
11038
11139
  return { command: process.execPath, args: [target] };
11039
11140
  }
11040
11141
  }
@@ -11043,7 +11144,7 @@ function resolveAgentCommand(cmd) {
11043
11144
  try {
11044
11145
  const fullPath = execFileSync("which", [cmd], { encoding: "utf-8" }).trim();
11045
11146
  if (fullPath) {
11046
- const content = fs9.readFileSync(fullPath, "utf-8");
11147
+ const content = fs8.readFileSync(fullPath, "utf-8");
11047
11148
  if (content.startsWith("#!/usr/bin/env node")) {
11048
11149
  return { command: process.execPath, args: [fullPath] };
11049
11150
  }
@@ -11475,8 +11576,8 @@ ${stderr}`
11475
11576
  writePath = result.path;
11476
11577
  writeContent = result.content;
11477
11578
  }
11478
- await fs9.promises.mkdir(path8.dirname(writePath), { recursive: true });
11479
- 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");
11480
11581
  return {};
11481
11582
  },
11482
11583
  // ── Terminal operations (delegated to TerminalManager) ─────────────
@@ -11578,7 +11679,7 @@ ${stderr}`
11578
11679
  [Attachment access denied: ${attCheck.reason}]`;
11579
11680
  continue;
11580
11681
  }
11581
- const data = await fs9.promises.readFile(att.filePath);
11682
+ const data = await fs8.promises.readFile(att.filePath);
11582
11683
  contentBlocks.push({ type: "image", data: data.toString("base64"), mimeType: att.mimeType });
11583
11684
  } else if (att.type === "audio" && this.promptCapabilities?.audio && !tooLarge) {
11584
11685
  const attCheck = this.pathGuard.validatePath(att.filePath, "read");
@@ -11588,7 +11689,7 @@ ${stderr}`
11588
11689
  [Attachment access denied: ${attCheck.reason}]`;
11589
11690
  continue;
11590
11691
  }
11591
- const data = await fs9.promises.readFile(att.filePath);
11692
+ const data = await fs8.promises.readFile(att.filePath);
11592
11693
  contentBlocks.push({ type: "audio", data: data.toString("base64"), mimeType: att.mimeType });
11593
11694
  } else {
11594
11695
  if ((att.type === "image" || att.type === "audio") && !tooLarge) {
@@ -11673,6 +11774,8 @@ var PromptQueue = class {
11673
11774
  queue = [];
11674
11775
  processing = false;
11675
11776
  abortController = null;
11777
+ /** Set when abort is triggered; drainNext waits for the current processor to settle before starting the next item. */
11778
+ processorSettled = null;
11676
11779
  async enqueue(text3, attachments, routing, turnId) {
11677
11780
  if (this.processing) {
11678
11781
  return new Promise((resolve6) => {
@@ -11685,6 +11788,10 @@ var PromptQueue = class {
11685
11788
  this.processing = true;
11686
11789
  this.abortController = new AbortController();
11687
11790
  const { signal } = this.abortController;
11791
+ let settledResolve;
11792
+ this.processorSettled = new Promise((r) => {
11793
+ settledResolve = r;
11794
+ });
11688
11795
  try {
11689
11796
  await Promise.race([
11690
11797
  this.processor(text3, attachments, routing, turnId),
@@ -11699,6 +11806,8 @@ var PromptQueue = class {
11699
11806
  } finally {
11700
11807
  this.abortController = null;
11701
11808
  this.processing = false;
11809
+ settledResolve();
11810
+ this.processorSettled = null;
11702
11811
  this.drainNext();
11703
11812
  }
11704
11813
  }
@@ -11794,7 +11903,7 @@ var PermissionGate = class {
11794
11903
 
11795
11904
  // src/core/sessions/session.ts
11796
11905
  init_log();
11797
- import * as fs10 from "fs";
11906
+ import * as fs9 from "fs";
11798
11907
 
11799
11908
  // src/core/sessions/turn-context.ts
11800
11909
  import { nanoid } from "nanoid";
@@ -11874,6 +11983,9 @@ var Session = class extends TypedEmitter {
11874
11983
  threadIds = /* @__PURE__ */ new Map();
11875
11984
  /** Active turn context — sealed on prompt dequeue, cleared on turn end */
11876
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;
11877
11989
  permissionGate = new PermissionGate();
11878
11990
  queue;
11879
11991
  speechService;
@@ -12024,6 +12136,7 @@ ${text3}`;
12024
12136
  });
12025
12137
  }
12026
12138
  let stopReason = "end_turn";
12139
+ let promptError;
12027
12140
  try {
12028
12141
  const response = await this.agentInstance.prompt(processed.text, processed.attachments);
12029
12142
  if (response && typeof response === "object" && "stopReason" in response) {
@@ -12035,14 +12148,21 @@ ${text3}`;
12035
12148
  if (ttsActive && this.voiceMode === "next") {
12036
12149
  this.voiceMode = "off";
12037
12150
  }
12151
+ } catch (err) {
12152
+ stopReason = "error";
12153
+ promptError = err;
12038
12154
  } finally {
12039
12155
  if (accumulatorListener) {
12040
12156
  this.off("agent_event", accumulatorListener);
12041
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;
12042
12163
  }
12043
- if (this.middlewareChain) {
12044
- this.middlewareChain.execute("turn:end", { sessionId: this.id, stopReason, durationMs: Date.now() - promptStart }, async (p) => p).catch(() => {
12045
- });
12164
+ if (promptError !== void 0) {
12165
+ throw promptError;
12046
12166
  }
12047
12167
  this.log.info(
12048
12168
  { durationMs: Date.now() - promptStart },
@@ -12053,7 +12173,6 @@ ${text3}`;
12053
12173
  this.log.warn({ err }, "TTS post-processing failed");
12054
12174
  });
12055
12175
  }
12056
- this.activeTurnContext = null;
12057
12176
  if (!this.name) {
12058
12177
  await this.autoName();
12059
12178
  }
@@ -12079,7 +12198,7 @@ ${text3}`;
12079
12198
  try {
12080
12199
  const audioPath = att.originalFilePath || att.filePath;
12081
12200
  const audioMime = att.originalFilePath ? "audio/ogg" : att.mimeType;
12082
- const audioBuffer = await fs10.promises.readFile(audioPath);
12201
+ const audioBuffer = await fs9.promises.readFile(audioPath);
12083
12202
  const result = await this.speechService.transcribe(audioBuffer, audioMime);
12084
12203
  this.log.info({ provider: "stt", duration: result.duration }, "Voice transcribed");
12085
12204
  this.emit("agent_event", {
@@ -12311,7 +12430,7 @@ ${result.text}` : result.text;
12311
12430
  };
12312
12431
 
12313
12432
  // src/core/message-transformer.ts
12314
- import * as path9 from "path";
12433
+ import * as path8 from "path";
12315
12434
 
12316
12435
  // src/core/utils/extract-file-info.ts
12317
12436
  function extractFileInfo(name, kind, content, rawInput, meta) {
@@ -12621,6 +12740,20 @@ var MessageTransformer = class {
12621
12740
  return { type: "text", text: "" };
12622
12741
  }
12623
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
+ }
12624
12757
  /** Check if rawInput is a non-empty object (not null, not {}) */
12625
12758
  isNonEmptyInput(input2) {
12626
12759
  return input2 !== null && input2 !== void 0 && typeof input2 === "object" && !Array.isArray(input2) && Object.keys(input2).length > 0;
@@ -12669,7 +12802,7 @@ var MessageTransformer = class {
12669
12802
  );
12670
12803
  return;
12671
12804
  }
12672
- const fileExt = path9.extname(fileInfo.filePath).toLowerCase();
12805
+ const fileExt = path8.extname(fileInfo.filePath).toLowerCase();
12673
12806
  if (BINARY_VIEWER_EXTENSIONS.has(fileExt)) {
12674
12807
  log5.debug({ kind, filePath: fileInfo.filePath }, "enrichWithViewerLinks: skipping binary file");
12675
12808
  return;
@@ -12815,6 +12948,7 @@ var SessionManager = class {
12815
12948
  } catch {
12816
12949
  }
12817
12950
  session.markCancelled();
12951
+ await session.destroy();
12818
12952
  this.sessions.delete(sessionId);
12819
12953
  }
12820
12954
  if (this.store) {
@@ -13016,9 +13150,12 @@ var SessionBridge = class {
13016
13150
  connect() {
13017
13151
  if (this.connected) return;
13018
13152
  this.connected = true;
13019
- this.listen(this.session.agentInstance, "agent_event", (event) => {
13020
- this.session.emit("agent_event", event);
13021
- });
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
+ }
13022
13159
  this.listen(this.session, "agent_event", (event) => {
13023
13160
  if (this.shouldForward(event)) {
13024
13161
  this.dispatchAgentEvent(event);
@@ -13097,6 +13234,7 @@ var SessionBridge = class {
13097
13234
  if (current?.__bridgeId === this.adapterId) {
13098
13235
  this.session.agentInstance.onPermissionRequest = async () => "";
13099
13236
  }
13237
+ this.deps.messageTransformer.clearSessionCaches?.(this.session.id);
13100
13238
  }
13101
13239
  /** Dispatch an agent event through middleware and to the adapter */
13102
13240
  async dispatchAgentEvent(event) {
@@ -13179,12 +13317,12 @@ var SessionBridge = class {
13179
13317
  break;
13180
13318
  case "image_content": {
13181
13319
  if (this.deps.fileService) {
13182
- const fs34 = this.deps.fileService;
13320
+ const fs33 = this.deps.fileService;
13183
13321
  const sid = this.session.id;
13184
13322
  const { data, mimeType } = event;
13185
13323
  const buffer = Buffer.from(data, "base64");
13186
- const ext = fs34.extensionFromMime(mimeType);
13187
- 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) => {
13188
13326
  this.sendMessage(sid, {
13189
13327
  type: "attachment",
13190
13328
  text: "",
@@ -13196,12 +13334,12 @@ var SessionBridge = class {
13196
13334
  }
13197
13335
  case "audio_content": {
13198
13336
  if (this.deps.fileService) {
13199
- const fs34 = this.deps.fileService;
13337
+ const fs33 = this.deps.fileService;
13200
13338
  const sid = this.session.id;
13201
13339
  const { data, mimeType } = event;
13202
13340
  const buffer = Buffer.from(data, "base64");
13203
- const ext = fs34.extensionFromMime(mimeType);
13204
- 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) => {
13205
13343
  this.sendMessage(sid, {
13206
13344
  type: "attachment",
13207
13345
  text: "",
@@ -13387,14 +13525,29 @@ var SessionFactory = class {
13387
13525
  }
13388
13526
  let agentInstance;
13389
13527
  try {
13390
- agentInstance = createParams.resumeAgentSessionId ? await this.agentManager.resume(
13391
- createParams.agentName,
13392
- createParams.workingDirectory,
13393
- createParams.resumeAgentSessionId
13394
- ) : await this.agentManager.spawn(
13395
- createParams.agentName,
13396
- createParams.workingDirectory
13397
- );
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
+ }
13398
13551
  } catch (err) {
13399
13552
  const message = err instanceof Error ? err.message : String(err);
13400
13553
  const guidanceLines = [
@@ -13423,31 +13576,9 @@ var SessionFactory = class {
13423
13576
  type: "system_message",
13424
13577
  message: guidanceLines.join("\n")
13425
13578
  };
13426
- const failedSession = new Session({
13427
- id: createParams.existingSessionId,
13428
- channelId: createParams.channelId,
13429
- agentName: createParams.agentName,
13430
- workingDirectory: createParams.workingDirectory,
13431
- // Dummy agent instance — will never be prompted
13432
- agentInstance: {
13433
- sessionId: "",
13434
- prompt: async () => {
13435
- },
13436
- cancel: async () => {
13437
- },
13438
- destroy: async () => {
13439
- },
13440
- on: () => {
13441
- },
13442
- off: () => {
13443
- }
13444
- },
13445
- speechService: this.speechService
13446
- });
13447
- this.sessionManager.registerSession(failedSession);
13448
- failedSession.emit("agent_event", guidance);
13579
+ const failedSessionId = createParams.existingSessionId ?? `failed-${Date.now()}`;
13449
13580
  this.eventBus.emit("agent:event", {
13450
- sessionId: failedSession.id,
13581
+ sessionId: failedSessionId,
13451
13582
  event: guidance
13452
13583
  });
13453
13584
  throw err;
@@ -13608,6 +13739,25 @@ var SessionFactory = class {
13608
13739
  session.setAgentCapabilities(record.acpState.agentCapabilities);
13609
13740
  }
13610
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
+ }
13611
13761
  log7.info({ sessionId: session.id, threadId }, "Lazy resume successful");
13612
13762
  return session;
13613
13763
  } catch (err) {
@@ -13732,14 +13882,14 @@ var SessionFactory = class {
13732
13882
  };
13733
13883
 
13734
13884
  // src/core/core.ts
13735
- import path17 from "path";
13885
+ import path16 from "path";
13736
13886
  import os8 from "os";
13737
13887
  import { nanoid as nanoid3 } from "nanoid";
13738
13888
 
13739
13889
  // src/core/sessions/session-store.ts
13740
13890
  init_log();
13741
- import fs11 from "fs";
13742
- import path10 from "path";
13891
+ import fs10 from "fs";
13892
+ import path9 from "path";
13743
13893
  var log8 = createChildLogger({ module: "session-store" });
13744
13894
  var DEBOUNCE_MS = 2e3;
13745
13895
  var JsonFileSessionStore = class {
@@ -13813,9 +13963,9 @@ var JsonFileSessionStore = class {
13813
13963
  version: 1,
13814
13964
  sessions: Object.fromEntries(this.records)
13815
13965
  };
13816
- const dir = path10.dirname(this.filePath);
13817
- if (!fs11.existsSync(dir)) fs11.mkdirSync(dir, { recursive: true });
13818
- 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));
13819
13969
  }
13820
13970
  destroy() {
13821
13971
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
@@ -13828,10 +13978,10 @@ var JsonFileSessionStore = class {
13828
13978
  }
13829
13979
  }
13830
13980
  load() {
13831
- if (!fs11.existsSync(this.filePath)) return;
13981
+ if (!fs10.existsSync(this.filePath)) return;
13832
13982
  try {
13833
13983
  const raw = JSON.parse(
13834
- fs11.readFileSync(this.filePath, "utf-8")
13984
+ fs10.readFileSync(this.filePath, "utf-8")
13835
13985
  );
13836
13986
  if (raw.version !== 1) {
13837
13987
  log8.warn(
@@ -13847,7 +13997,7 @@ var JsonFileSessionStore = class {
13847
13997
  } catch (err) {
13848
13998
  log8.error({ err }, "Failed to load session store, backing up corrupt file");
13849
13999
  try {
13850
- fs11.renameSync(this.filePath, `${this.filePath}.bak`);
14000
+ fs10.renameSync(this.filePath, `${this.filePath}.bak`);
13851
14001
  } catch {
13852
14002
  }
13853
14003
  }
@@ -13871,7 +14021,10 @@ var JsonFileSessionStore = class {
13871
14021
  for (const [id, record] of this.records) {
13872
14022
  if (record.status === "active" || record.status === "initializing")
13873
14023
  continue;
13874
- 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;
13875
14028
  if (lastActive < cutoff) {
13876
14029
  this.records.delete(id);
13877
14030
  removed++;
@@ -13929,8 +14082,8 @@ var AgentSwitchHandler = class {
13929
14082
  if (middlewareChain && !result) throw new Error("Agent switch blocked by middleware");
13930
14083
  const lastEntry = session.findLastSwitchEntry(toAgent);
13931
14084
  const caps = getAgentCapabilities(toAgent);
13932
- const canResume = !!(lastEntry && caps.supportsResume && lastEntry.promptCount === 0);
13933
- const resumed = canResume;
14085
+ const canResume = !!(lastEntry && caps.supportsResume);
14086
+ let resumed = false;
13934
14087
  const startEvent = {
13935
14088
  type: "system_message",
13936
14089
  message: `Switching from ${fromAgent} to ${toAgent}...`
@@ -13964,29 +14117,34 @@ var AgentSwitchHandler = class {
13964
14117
  try {
13965
14118
  await session.switchAgent(toAgent, async () => {
13966
14119
  if (canResume) {
13967
- const instance = await agentManager.resume(toAgent, session.workingDirectory, lastEntry.agentSessionId);
13968
- if (fileService) instance.addAllowedPath(fileService.baseDir);
13969
- return instance;
13970
- } else {
13971
- const instance = await agentManager.spawn(toAgent, session.workingDirectory);
13972
- if (fileService) instance.addAllowedPath(fileService.baseDir);
13973
14120
  try {
13974
- const contextService = this.deps.getService("context");
13975
- if (contextService) {
13976
- const config = configManager.get();
13977
- const labelAgent = config.agentSwitch?.labelHistory ?? true;
13978
- const contextResult = await contextService.buildContext(
13979
- { type: "session", value: sessionId, repoPath: session.workingDirectory },
13980
- { labelAgent }
13981
- );
13982
- if (contextResult?.markdown) {
13983
- session.setContext(contextResult.markdown);
13984
- }
13985
- }
14121
+ const instance2 = await agentManager.resume(toAgent, session.workingDirectory, lastEntry.agentSessionId);
14122
+ if (fileService) instance2.addAllowedPath(fileService.baseDir);
14123
+ resumed = true;
14124
+ return instance2;
13986
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
+ }
13987
14144
  }
13988
- return instance;
14145
+ } catch {
13989
14146
  }
14147
+ return instance;
13990
14148
  });
13991
14149
  const successEvent = {
13992
14150
  type: "system_message",
@@ -14070,14 +14228,14 @@ var AgentSwitchHandler = class {
14070
14228
  };
14071
14229
 
14072
14230
  // src/core/agents/agent-catalog.ts
14073
- import * as fs15 from "fs";
14074
- import * as path14 from "path";
14231
+ import * as fs14 from "fs";
14232
+ import * as path13 from "path";
14075
14233
  import * as os6 from "os";
14076
14234
 
14077
14235
  // src/core/agents/agent-store.ts
14078
14236
  init_log();
14079
- import * as fs13 from "fs";
14080
- import * as path12 from "path";
14237
+ import * as fs12 from "fs";
14238
+ import * as path11 from "path";
14081
14239
  import * as os4 from "os";
14082
14240
  import { z as z2 } from "zod";
14083
14241
  var log10 = createChildLogger({ module: "agent-store" });
@@ -14101,15 +14259,15 @@ var AgentStore = class {
14101
14259
  data = { version: 1, installed: {} };
14102
14260
  filePath;
14103
14261
  constructor(filePath) {
14104
- this.filePath = filePath ?? path12.join(os4.homedir(), ".openacp", "agents.json");
14262
+ this.filePath = filePath ?? path11.join(os4.homedir(), ".openacp", "agents.json");
14105
14263
  }
14106
14264
  load() {
14107
- if (!fs13.existsSync(this.filePath)) {
14265
+ if (!fs12.existsSync(this.filePath)) {
14108
14266
  this.data = { version: 1, installed: {} };
14109
14267
  return;
14110
14268
  }
14111
14269
  try {
14112
- const raw = JSON.parse(fs13.readFileSync(this.filePath, "utf-8"));
14270
+ const raw = JSON.parse(fs12.readFileSync(this.filePath, "utf-8"));
14113
14271
  const result = AgentStoreSchema.safeParse(raw);
14114
14272
  if (result.success) {
14115
14273
  this.data = result.data;
@@ -14123,7 +14281,7 @@ var AgentStore = class {
14123
14281
  }
14124
14282
  }
14125
14283
  exists() {
14126
- return fs13.existsSync(this.filePath);
14284
+ return fs12.existsSync(this.filePath);
14127
14285
  }
14128
14286
  getInstalled() {
14129
14287
  return this.data.installed;
@@ -14143,10 +14301,10 @@ var AgentStore = class {
14143
14301
  return key in this.data.installed;
14144
14302
  }
14145
14303
  save() {
14146
- fs13.mkdirSync(path12.dirname(this.filePath), { recursive: true });
14304
+ fs12.mkdirSync(path11.dirname(this.filePath), { recursive: true });
14147
14305
  const tmpPath = this.filePath + ".tmp";
14148
- fs13.writeFileSync(tmpPath, JSON.stringify(this.data, null, 2), { mode: 384 });
14149
- fs13.renameSync(tmpPath, this.filePath);
14306
+ fs12.writeFileSync(tmpPath, JSON.stringify(this.data, null, 2), { mode: 384 });
14307
+ fs12.renameSync(tmpPath, this.filePath);
14150
14308
  }
14151
14309
  };
14152
14310
 
@@ -14164,7 +14322,7 @@ var AgentCatalog = class {
14164
14322
  agentsDir;
14165
14323
  constructor(store, cachePath, agentsDir) {
14166
14324
  this.store = store ?? new AgentStore();
14167
- this.cachePath = cachePath ?? path14.join(os6.homedir(), ".openacp", "registry-cache.json");
14325
+ this.cachePath = cachePath ?? path13.join(os6.homedir(), ".openacp", "registry-cache.json");
14168
14326
  this.agentsDir = agentsDir;
14169
14327
  }
14170
14328
  load() {
@@ -14185,8 +14343,8 @@ var AgentCatalog = class {
14185
14343
  ttlHours: DEFAULT_TTL_HOURS,
14186
14344
  data
14187
14345
  };
14188
- fs15.mkdirSync(path14.dirname(this.cachePath), { recursive: true });
14189
- 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 });
14190
14348
  log12.info({ count: this.registryAgents.length }, "Registry updated");
14191
14349
  } catch (err) {
14192
14350
  log12.warn({ err }, "Failed to fetch registry, using cached data");
@@ -14354,9 +14512,9 @@ var AgentCatalog = class {
14354
14512
  }
14355
14513
  }
14356
14514
  isCacheStale() {
14357
- if (!fs15.existsSync(this.cachePath)) return true;
14515
+ if (!fs14.existsSync(this.cachePath)) return true;
14358
14516
  try {
14359
- const raw = JSON.parse(fs15.readFileSync(this.cachePath, "utf-8"));
14517
+ const raw = JSON.parse(fs14.readFileSync(this.cachePath, "utf-8"));
14360
14518
  const fetchedAt = new Date(raw.fetchedAt).getTime();
14361
14519
  const ttlMs = (raw.ttlHours ?? DEFAULT_TTL_HOURS) * 60 * 60 * 1e3;
14362
14520
  return Date.now() - fetchedAt > ttlMs;
@@ -14365,9 +14523,9 @@ var AgentCatalog = class {
14365
14523
  }
14366
14524
  }
14367
14525
  loadRegistryFromCacheOrSnapshot() {
14368
- if (fs15.existsSync(this.cachePath)) {
14526
+ if (fs14.existsSync(this.cachePath)) {
14369
14527
  try {
14370
- const raw = JSON.parse(fs15.readFileSync(this.cachePath, "utf-8"));
14528
+ const raw = JSON.parse(fs14.readFileSync(this.cachePath, "utf-8"));
14371
14529
  if (raw.data?.agents) {
14372
14530
  this.registryAgents = raw.data.agents;
14373
14531
  log12.debug({ count: this.registryAgents.length }, "Loaded registry from cache");
@@ -14379,13 +14537,13 @@ var AgentCatalog = class {
14379
14537
  }
14380
14538
  try {
14381
14539
  const candidates = [
14382
- path14.join(import.meta.dirname, "data", "registry-snapshot.json"),
14383
- path14.join(import.meta.dirname, "..", "data", "registry-snapshot.json"),
14384
- 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")
14385
14543
  ];
14386
14544
  for (const candidate of candidates) {
14387
- if (fs15.existsSync(candidate)) {
14388
- const raw = JSON.parse(fs15.readFileSync(candidate, "utf-8"));
14545
+ if (fs14.existsSync(candidate)) {
14546
+ const raw = JSON.parse(fs14.readFileSync(candidate, "utf-8"));
14389
14547
  this.registryAgents = raw.agents ?? [];
14390
14548
  log12.debug({ count: this.registryAgents.length }, "Loaded registry from bundled snapshot");
14391
14549
  return;
@@ -14646,31 +14804,31 @@ var ErrorTracker = class {
14646
14804
  };
14647
14805
 
14648
14806
  // src/core/plugin/plugin-context.ts
14649
- import path16 from "path";
14807
+ import path15 from "path";
14650
14808
  import os7 from "os";
14651
14809
 
14652
14810
  // src/core/plugin/plugin-storage.ts
14653
- import fs16 from "fs";
14654
- import path15 from "path";
14811
+ import fs15 from "fs";
14812
+ import path14 from "path";
14655
14813
  var PluginStorageImpl = class {
14656
14814
  kvPath;
14657
14815
  dataDir;
14658
14816
  writeChain = Promise.resolve();
14659
14817
  constructor(baseDir) {
14660
- this.dataDir = path15.join(baseDir, "data");
14661
- this.kvPath = path15.join(baseDir, "kv.json");
14662
- 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 });
14663
14821
  }
14664
14822
  readKv() {
14665
14823
  try {
14666
- const raw = fs16.readFileSync(this.kvPath, "utf-8");
14824
+ const raw = fs15.readFileSync(this.kvPath, "utf-8");
14667
14825
  return JSON.parse(raw);
14668
14826
  } catch {
14669
14827
  return {};
14670
14828
  }
14671
14829
  }
14672
14830
  writeKv(data) {
14673
- fs16.writeFileSync(this.kvPath, JSON.stringify(data), "utf-8");
14831
+ fs15.writeFileSync(this.kvPath, JSON.stringify(data), "utf-8");
14674
14832
  }
14675
14833
  async get(key) {
14676
14834
  const data = this.readKv();
@@ -14696,7 +14854,7 @@ var PluginStorageImpl = class {
14696
14854
  return Object.keys(this.readKv());
14697
14855
  }
14698
14856
  getDataDir() {
14699
- fs16.mkdirSync(this.dataDir, { recursive: true });
14857
+ fs15.mkdirSync(this.dataDir, { recursive: true });
14700
14858
  return this.dataDir;
14701
14859
  }
14702
14860
  };
@@ -14720,9 +14878,11 @@ function createPluginContext(opts) {
14720
14878
  config,
14721
14879
  core
14722
14880
  } = opts;
14723
- const instanceRoot = opts.instanceRoot ?? path16.join(os7.homedir(), ".openacp");
14881
+ const instanceRoot = opts.instanceRoot ?? path15.join(os7.homedir(), ".openacp");
14724
14882
  const registeredListeners = [];
14725
14883
  const registeredCommands = [];
14884
+ const registeredMenuItemIds = [];
14885
+ const registeredAssistantSectionIds = [];
14726
14886
  const noopLog = {
14727
14887
  trace() {
14728
14888
  },
@@ -14817,7 +14977,9 @@ function createPluginContext(opts) {
14817
14977
  requirePermission(permissions, "commands:register", "registerMenuItem()");
14818
14978
  const menuRegistry = serviceRegistry.get("menu-registry");
14819
14979
  if (!menuRegistry) return;
14820
- menuRegistry.register({ ...item, id: `${pluginName}:${item.id}` });
14980
+ const qualifiedId = `${pluginName}:${item.id}`;
14981
+ menuRegistry.register({ ...item, id: qualifiedId });
14982
+ registeredMenuItemIds.push(qualifiedId);
14821
14983
  },
14822
14984
  unregisterMenuItem(id) {
14823
14985
  requirePermission(permissions, "commands:register", "unregisterMenuItem()");
@@ -14829,7 +14991,9 @@ function createPluginContext(opts) {
14829
14991
  requirePermission(permissions, "commands:register", "registerAssistantSection()");
14830
14992
  const assistantRegistry = serviceRegistry.get("assistant-registry");
14831
14993
  if (!assistantRegistry) return;
14832
- assistantRegistry.register({ ...section, id: `${pluginName}:${section.id}` });
14994
+ const qualifiedId = `${pluginName}:${section.id}`;
14995
+ assistantRegistry.register({ ...section, id: qualifiedId });
14996
+ registeredAssistantSectionIds.push(qualifiedId);
14833
14997
  },
14834
14998
  unregisterAssistantSection(id) {
14835
14999
  requirePermission(permissions, "commands:register", "unregisterAssistantSection()");
@@ -14837,6 +15001,14 @@ function createPluginContext(opts) {
14837
15001
  if (!assistantRegistry) return;
14838
15002
  assistantRegistry.unregister(`${pluginName}:${id}`);
14839
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
+ },
14840
15012
  get sessions() {
14841
15013
  requirePermission(permissions, "kernel:access", "sessions");
14842
15014
  return sessions;
@@ -14865,6 +15037,20 @@ function createPluginContext(opts) {
14865
15037
  if (cmdRegistry && typeof cmdRegistry.unregisterByPlugin === "function") {
14866
15038
  cmdRegistry.unregisterByPlugin(pluginName);
14867
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;
14868
15054
  registeredCommands.length = 0;
14869
15055
  }
14870
15056
  };
@@ -14899,11 +15085,8 @@ function resolvePluginConfig(pluginName, configManager) {
14899
15085
  "@openacp/file-service": "files",
14900
15086
  "@openacp/api-server": "api",
14901
15087
  "@openacp/telegram": "channels.telegram",
14902
- "@openacp/discord": "channels.discord",
14903
- "@openacp/adapter-discord": "channels.discord",
14904
- "@openacp/plugin-discord": "channels.discord",
14905
- // alias for old name
14906
- "@openacp/slack": "channels.slack"
15088
+ "@openacp/discord-adapter": "channels.discord",
15089
+ "@openacp/slack-adapter": "channels.slack"
14907
15090
  };
14908
15091
  const legacyKey = legacyMap[pluginName];
14909
15092
  if (legacyKey) {
@@ -15388,8 +15571,10 @@ function createConfigSection(core) {
15388
15571
  priority: 30,
15389
15572
  buildContext: () => {
15390
15573
  const config = core.configManager.get();
15574
+ const speechSvc = core.lifecycleManager?.serviceRegistry.get("speech");
15575
+ const sttActive = speechSvc ? speechSvc.isSTTAvailable() : false;
15391
15576
  return `Workspace base: ${config.workspace.baseDir}
15392
- STT: ${config.speech?.stt?.provider ? `${config.speech.stt.provider} \u2705` : "Not configured"}`;
15577
+ STT: ${sttActive ? "configured \u2705" : "Not configured"}`;
15393
15578
  },
15394
15579
  commands: [
15395
15580
  { command: "openacp config", description: "View config" },
@@ -15550,7 +15735,7 @@ var OpenACPCore = class {
15550
15735
  );
15551
15736
  this.agentCatalog.load();
15552
15737
  this.agentManager = new AgentManager(this.agentCatalog);
15553
- 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");
15554
15739
  this.sessionStore = new JsonFileSessionStore(
15555
15740
  storePath,
15556
15741
  config.sessionStore.ttlDays
@@ -15574,7 +15759,7 @@ var OpenACPCore = class {
15574
15759
  sessions: this.sessionManager,
15575
15760
  config: this.configManager,
15576
15761
  core: this,
15577
- storagePath: ctx?.paths.pluginsData ?? path17.join(os8.homedir(), ".openacp", "plugins", "data"),
15762
+ storagePath: ctx?.paths.pluginsData ?? path16.join(os8.homedir(), ".openacp", "plugins", "data"),
15578
15763
  instanceRoot: ctx?.root,
15579
15764
  log: createChildLogger({ module: "plugin" })
15580
15765
  });
@@ -15640,7 +15825,7 @@ var OpenACPCore = class {
15640
15825
  );
15641
15826
  registerCoreMenuItems(this.menuRegistry);
15642
15827
  if (ctx?.root) {
15643
- this.assistantRegistry.setInstanceRoot(path17.dirname(ctx.root));
15828
+ this.assistantRegistry.setInstanceRoot(path16.dirname(ctx.root));
15644
15829
  }
15645
15830
  this.assistantRegistry.register(createSessionsSection(this));
15646
15831
  this.assistantRegistry.register(createAgentsSection(this));
@@ -15907,15 +16092,15 @@ ${text3}`;
15907
16092
  message: `Agent '${agentName}' not found`
15908
16093
  };
15909
16094
  }
15910
- const { existsSync: existsSync20 } = await import("fs");
15911
- if (!existsSync20(cwd)) {
16095
+ const { existsSync: existsSync19 } = await import("fs");
16096
+ if (!existsSync19(cwd)) {
15912
16097
  return {
15913
16098
  ok: false,
15914
16099
  error: "invalid_cwd",
15915
16100
  message: `Directory does not exist: ${cwd}`
15916
16101
  };
15917
16102
  }
15918
- const maxSessions = this.configManager.get().security.maxConcurrentSessions;
16103
+ const maxSessions = 20;
15919
16104
  if (this.sessionManager.listSessions().length >= maxSessions) {
15920
16105
  return {
15921
16106
  ok: false,
@@ -16172,7 +16357,9 @@ var CommandRegistry = class _CommandRegistry {
16172
16357
  this.commands.delete(name);
16173
16358
  if (cmd.scope) {
16174
16359
  this.commands.delete(`${cmd.scope}:${cmd.name}`);
16175
- this.commands.delete(cmd.name);
16360
+ if (this.commands.get(cmd.name) === cmd) {
16361
+ this.commands.delete(cmd.name);
16362
+ }
16176
16363
  }
16177
16364
  }
16178
16365
  /** Remove all commands registered by a given plugin. */
@@ -16252,19 +16439,19 @@ init_doctor();
16252
16439
  init_config_registry();
16253
16440
 
16254
16441
  // src/core/config/config-editor.ts
16255
- import * as path29 from "path";
16442
+ import * as path28 from "path";
16256
16443
  import * as clack2 from "@clack/prompts";
16257
16444
 
16258
16445
  // src/cli/autostart.ts
16259
16446
  init_log();
16260
- import { execFileSync as execFileSync5 } from "child_process";
16261
- import * as fs27 from "fs";
16262
- 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";
16263
16450
  import * as os11 from "os";
16264
16451
  var log18 = createChildLogger({ module: "autostart" });
16265
16452
  var LAUNCHD_LABEL = "com.openacp.daemon";
16266
- var LAUNCHD_PLIST_PATH = path25.join(os11.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
16267
- 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");
16268
16455
  function isAutoStartSupported() {
16269
16456
  return process.platform === "darwin" || process.platform === "linux";
16270
16457
  }
@@ -16276,7 +16463,7 @@ function escapeSystemdValue(str) {
16276
16463
  return `"${escaped}"`;
16277
16464
  }
16278
16465
  function generateLaunchdPlist(nodePath, cliPath, logDir2) {
16279
- const logFile = path25.join(logDir2, "openacp.log");
16466
+ const logFile = path24.join(logDir2, "openacp.log");
16280
16467
  return `<?xml version="1.0" encoding="UTF-8"?>
16281
16468
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
16282
16469
  <plist version="1.0">
@@ -16321,25 +16508,25 @@ function installAutoStart(logDir2) {
16321
16508
  return { success: false, error: "Auto-start not supported on this platform" };
16322
16509
  }
16323
16510
  const nodePath = process.execPath;
16324
- const cliPath = path25.resolve(process.argv[1]);
16325
- 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;
16326
16513
  try {
16327
16514
  if (process.platform === "darwin") {
16328
16515
  const plist = generateLaunchdPlist(nodePath, cliPath, resolvedLogDir);
16329
- const dir = path25.dirname(LAUNCHD_PLIST_PATH);
16330
- fs27.mkdirSync(dir, { recursive: true });
16331
- fs27.writeFileSync(LAUNCHD_PLIST_PATH, plist);
16332
- 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" });
16333
16520
  log18.info("LaunchAgent installed");
16334
16521
  return { success: true };
16335
16522
  }
16336
16523
  if (process.platform === "linux") {
16337
16524
  const unit = generateSystemdUnit(nodePath, cliPath);
16338
- const dir = path25.dirname(SYSTEMD_SERVICE_PATH);
16339
- fs27.mkdirSync(dir, { recursive: true });
16340
- fs27.writeFileSync(SYSTEMD_SERVICE_PATH, unit);
16341
- execFileSync5("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
16342
- 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" });
16343
16530
  log18.info("systemd user service installed");
16344
16531
  return { success: true };
16345
16532
  }
@@ -16356,24 +16543,24 @@ function uninstallAutoStart() {
16356
16543
  }
16357
16544
  try {
16358
16545
  if (process.platform === "darwin") {
16359
- if (fs27.existsSync(LAUNCHD_PLIST_PATH)) {
16546
+ if (fs26.existsSync(LAUNCHD_PLIST_PATH)) {
16360
16547
  try {
16361
- execFileSync5("launchctl", ["unload", LAUNCHD_PLIST_PATH], { stdio: "pipe" });
16548
+ execFileSync6("launchctl", ["unload", LAUNCHD_PLIST_PATH], { stdio: "pipe" });
16362
16549
  } catch {
16363
16550
  }
16364
- fs27.unlinkSync(LAUNCHD_PLIST_PATH);
16551
+ fs26.unlinkSync(LAUNCHD_PLIST_PATH);
16365
16552
  log18.info("LaunchAgent removed");
16366
16553
  }
16367
16554
  return { success: true };
16368
16555
  }
16369
16556
  if (process.platform === "linux") {
16370
- if (fs27.existsSync(SYSTEMD_SERVICE_PATH)) {
16557
+ if (fs26.existsSync(SYSTEMD_SERVICE_PATH)) {
16371
16558
  try {
16372
- execFileSync5("systemctl", ["--user", "disable", "openacp"], { stdio: "pipe" });
16559
+ execFileSync6("systemctl", ["--user", "disable", "openacp"], { stdio: "pipe" });
16373
16560
  } catch {
16374
16561
  }
16375
- fs27.unlinkSync(SYSTEMD_SERVICE_PATH);
16376
- execFileSync5("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
16562
+ fs26.unlinkSync(SYSTEMD_SERVICE_PATH);
16563
+ execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
16377
16564
  log18.info("systemd user service removed");
16378
16565
  }
16379
16566
  return { success: true };
@@ -16387,10 +16574,10 @@ function uninstallAutoStart() {
16387
16574
  }
16388
16575
  function isAutoStartInstalled() {
16389
16576
  if (process.platform === "darwin") {
16390
- return fs27.existsSync(LAUNCHD_PLIST_PATH);
16577
+ return fs26.existsSync(LAUNCHD_PLIST_PATH);
16391
16578
  }
16392
16579
  if (process.platform === "linux") {
16393
- return fs27.existsSync(SYSTEMD_SERVICE_PATH);
16580
+ return fs26.existsSync(SYSTEMD_SERVICE_PATH);
16394
16581
  }
16395
16582
  return false;
16396
16583
  }
@@ -16441,42 +16628,19 @@ var header = (title) => `
16441
16628
  ${c.cyan}${c.bold}[${title}]${c.reset}
16442
16629
  `;
16443
16630
  async function editTelegram(config, updates, settingsManager) {
16444
- const tg = config.channels?.telegram ?? {};
16445
- let currentToken = tg.botToken ?? "";
16446
- let currentChatId = tg.chatId ?? 0;
16447
- let currentEnabled = tg.enabled ?? false;
16448
- if (settingsManager) {
16449
- const ps = await settingsManager.loadSettings("@openacp/telegram");
16450
- if (Object.keys(ps).length > 0) {
16451
- currentToken = ps.botToken ?? currentToken;
16452
- currentChatId = ps.chatId ?? currentChatId;
16453
- currentEnabled = ps.enabled ?? currentEnabled;
16454
- }
16455
- }
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;
16456
16635
  console.log(header("Telegram"));
16457
- console.log(` Enabled : ${currentEnabled ? ok("yes") : dim("no")}`);
16458
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")}`);
16459
16638
  console.log(` Bot Token : ${tokenDisplay}`);
16460
16639
  console.log(` Chat ID : ${currentChatId || dim("(not set)")}`);
16461
16640
  console.log("");
16462
- const ensureTelegramUpdates = () => {
16463
- if (!updates.channels) updates.channels = {};
16464
- if (!updates.channels.telegram) {
16465
- updates.channels.telegram = {};
16466
- }
16467
- return updates.channels.telegram;
16468
- };
16469
16641
  while (true) {
16470
- const isEnabled = await (async () => {
16471
- if (settingsManager) {
16472
- const ps = await settingsManager.loadSettings("@openacp/telegram");
16473
- if ("enabled" in ps) return ps.enabled;
16474
- }
16475
- const ch = updates.channels;
16476
- const tgUp = ch?.telegram;
16477
- if (tgUp && "enabled" in tgUp) return tgUp.enabled;
16478
- return currentEnabled;
16479
- })();
16642
+ const freshSettings = settingsManager ? await settingsManager.loadSettings("@openacp/telegram") : ps;
16643
+ const isEnabled = freshSettings.enabled ?? currentEnabled;
16480
16644
  const choice = await select3({
16481
16645
  message: "Telegram settings:",
16482
16646
  choices: [
@@ -16490,75 +16654,32 @@ async function editTelegram(config, updates, settingsManager) {
16490
16654
  if (choice === "toggle") {
16491
16655
  if (settingsManager) {
16492
16656
  await settingsManager.updatePluginSettings("@openacp/telegram", { enabled: !isEnabled });
16493
- } else {
16494
- const tgUp = ensureTelegramUpdates();
16495
- tgUp.enabled = !isEnabled;
16657
+ console.log(!isEnabled ? ok("Telegram enabled") : ok("Telegram disabled"));
16496
16658
  }
16497
- console.log(!isEnabled ? ok("Telegram enabled") : ok("Telegram disabled"));
16498
16659
  }
16499
16660
  if (choice === "token") {
16500
16661
  const token = await input({
16501
16662
  message: "New bot token:",
16502
- default: currentToken,
16503
16663
  validate: (val) => val.trim().length > 0 || "Token cannot be empty"
16504
16664
  });
16505
- try {
16506
- const { validateBotToken: validateBotToken2 } = await Promise.resolve().then(() => (init_validators(), validators_exports));
16507
- const result = await validateBotToken2(token.trim());
16508
- if (result.ok) {
16509
- console.log(ok(`Connected to @${result.botUsername}`));
16510
- } else {
16511
- console.log(warn(`Validation failed: ${result.error} \u2014 saving anyway`));
16512
- }
16513
- } catch {
16514
- console.log(warn("Telegram validator not available \u2014 skipping validation"));
16515
- }
16516
16665
  if (settingsManager) {
16517
- await settingsManager.updatePluginSettings("@openacp/telegram", { botToken: token.trim(), enabled: true });
16518
- } else {
16519
- const tgUp = ensureTelegramUpdates();
16520
- tgUp.botToken = token.trim();
16521
- tgUp.enabled = true;
16666
+ await settingsManager.updatePluginSettings("@openacp/telegram", { botToken: token.trim() });
16667
+ console.log(ok("Bot token updated"));
16522
16668
  }
16523
16669
  }
16524
16670
  if (choice === "chatid") {
16525
- const chatIdStr = await input({
16526
- message: "New chat ID (e.g. -1001234567890):",
16527
- default: String(currentChatId),
16528
- validate: (val) => {
16529
- const n = Number(val.trim());
16530
- if (isNaN(n) || !Number.isInteger(n)) return "Chat ID must be an integer";
16531
- return true;
16532
- }
16671
+ const chatId = await input({
16672
+ message: "New chat ID:",
16673
+ validate: (val) => !isNaN(Number(val.trim())) || "Must be a number"
16533
16674
  });
16534
- const chatId = Number(chatIdStr.trim());
16535
- const tokenForValidation = (() => {
16536
- const ch = updates.channels;
16537
- const tgUp = ch?.telegram;
16538
- if (typeof tgUp?.botToken === "string") return tgUp.botToken;
16539
- return currentToken;
16540
- })();
16541
- try {
16542
- const { validateChatId: validateChatId2 } = await Promise.resolve().then(() => (init_validators(), validators_exports));
16543
- const result = await validateChatId2(tokenForValidation, chatId);
16544
- if (result.ok) {
16545
- console.log(ok(`Group: ${result.title}${result.isForum ? "" : warn(" (topics not enabled)")}`));
16546
- } else {
16547
- console.log(warn(`Validation failed: ${result.error} \u2014 saving anyway`));
16548
- }
16549
- } catch {
16550
- console.log(warn("Telegram validator not available \u2014 skipping validation"));
16551
- }
16552
16675
  if (settingsManager) {
16553
- await settingsManager.updatePluginSettings("@openacp/telegram", { chatId });
16554
- } else {
16555
- const tgUp = ensureTelegramUpdates();
16556
- tgUp.chatId = chatId;
16676
+ await settingsManager.updatePluginSettings("@openacp/telegram", { chatId: Number(chatId.trim()) });
16677
+ console.log(ok(`Chat ID set to ${chatId.trim()}`));
16557
16678
  }
16558
16679
  }
16559
16680
  }
16560
16681
  }
16561
- var DISCORD_PACKAGE = "@openacp/adapter-discord";
16682
+ var DISCORD_PACKAGE = "@openacp/discord-adapter";
16562
16683
  async function ensureDiscordPlugin() {
16563
16684
  try {
16564
16685
  return await import(DISCORD_PACKAGE);
@@ -16595,7 +16716,7 @@ async function editDiscord(_config, _updates) {
16595
16716
  const { createInstallContext: createInstallContext2 } = await Promise.resolve().then(() => (init_install_context(), install_context_exports));
16596
16717
  const { getGlobalRoot: getGlobalRoot2 } = await Promise.resolve().then(() => (init_instance_context(), instance_context_exports));
16597
16718
  const root = getGlobalRoot2();
16598
- const basePath = path29.join(root, "plugins", "data");
16719
+ const basePath = path28.join(root, "plugins", "data");
16599
16720
  const settingsManager = new SettingsManager2(basePath);
16600
16721
  const ctx = createInstallContext2({
16601
16722
  pluginName: plugin.name,
@@ -16609,12 +16730,12 @@ async function editDiscord(_config, _updates) {
16609
16730
  }
16610
16731
  }
16611
16732
  async function editChannels(config, updates, settingsManager) {
16612
- let tgConfigured = !!config.channels?.telegram;
16613
- let dcConfigured = !!config.channels?.discord;
16733
+ let tgConfigured = false;
16734
+ let dcConfigured = false;
16614
16735
  if (settingsManager) {
16615
16736
  const tgPs = await settingsManager.loadSettings("@openacp/telegram");
16616
16737
  if (tgPs.botToken && tgPs.chatId) tgConfigured = true;
16617
- const dcPs = await settingsManager.loadSettings("@openacp/adapter-discord");
16738
+ const dcPs = await settingsManager.loadSettings("@openacp/discord-adapter");
16618
16739
  if (dcPs.guildId || dcPs.token) dcConfigured = true;
16619
16740
  }
16620
16741
  console.log(header("Channels"));
@@ -16636,7 +16757,7 @@ async function editChannels(config, updates, settingsManager) {
16636
16757
  }
16637
16758
  }
16638
16759
  async function editAgent(config, updates) {
16639
- const agentNames = Object.keys(config.agents ?? {});
16760
+ const agentNames = [];
16640
16761
  const currentDefault = config.defaultAgent;
16641
16762
  console.log(header("Agent"));
16642
16763
  console.log(` Default agent : ${c.bold}${currentDefault}${c.reset}`);
@@ -16679,17 +16800,12 @@ async function editWorkspace(config, updates) {
16679
16800
  console.log(ok(`Workspace set to ${newDir.trim()}`));
16680
16801
  }
16681
16802
  async function editSecurity(config, updates, settingsManager) {
16682
- let sec = config.security ?? { allowedUserIds: [], maxConcurrentSessions: 20, sessionTimeoutMinutes: 60 };
16683
- if (settingsManager) {
16684
- const ps = await settingsManager.loadSettings("@openacp/security");
16685
- if (Object.keys(ps).length > 0) {
16686
- sec = {
16687
- allowedUserIds: ps.allowedUserIds ?? sec.allowedUserIds,
16688
- maxConcurrentSessions: ps.maxConcurrentSessions ?? sec.maxConcurrentSessions,
16689
- sessionTimeoutMinutes: ps.sessionTimeoutMinutes ?? sec.sessionTimeoutMinutes
16690
- };
16691
- }
16692
- }
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
+ };
16693
16809
  console.log(header("Security"));
16694
16810
  console.log(` Allowed user IDs : ${sec.allowedUserIds?.length ? sec.allowedUserIds.join(", ") : dim("(all users allowed)")}`);
16695
16811
  console.log(` Max concurrent sessions : ${sec.maxConcurrentSessions}`);
@@ -16717,9 +16833,6 @@ async function editSecurity(config, updates, settingsManager) {
16717
16833
  });
16718
16834
  if (settingsManager) {
16719
16835
  await settingsManager.updatePluginSettings("@openacp/security", { maxConcurrentSessions: Number(val.trim()) });
16720
- } else {
16721
- if (!updates.security) updates.security = {};
16722
- updates.security.maxConcurrentSessions = Number(val.trim());
16723
16836
  }
16724
16837
  console.log(ok(`Max concurrent sessions set to ${val.trim()}`));
16725
16838
  }
@@ -16735,9 +16848,6 @@ async function editSecurity(config, updates, settingsManager) {
16735
16848
  });
16736
16849
  if (settingsManager) {
16737
16850
  await settingsManager.updatePluginSettings("@openacp/security", { sessionTimeoutMinutes: Number(val.trim()) });
16738
- } else {
16739
- if (!updates.security) updates.security = {};
16740
- updates.security.sessionTimeoutMinutes = Number(val.trim());
16741
16851
  }
16742
16852
  console.log(ok(`Session timeout set to ${val.trim()} minutes`));
16743
16853
  }
@@ -16864,20 +16974,16 @@ async function editRunMode(config, updates) {
16864
16974
  }
16865
16975
  }
16866
16976
  async function editApi(config, updates, settingsManager) {
16867
- let api = config.api ?? { port: 21420, host: "127.0.0.1" };
16868
- if (settingsManager) {
16869
- const ps = await settingsManager.loadSettings("@openacp/api-server");
16870
- if (Object.keys(ps).length > 0) {
16871
- api = { port: ps.port ?? api.port, host: ps.host ?? api.host };
16872
- }
16873
- }
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";
16874
16980
  console.log(header("API"));
16875
- console.log(` Port : ${api.port}`);
16876
- console.log(` Host : ${api.host} ${dim("(localhost only)")}`);
16981
+ console.log(` Port : ${currentPort}`);
16982
+ console.log(` Host : ${currentHost} ${dim("(localhost only)")}`);
16877
16983
  console.log("");
16878
16984
  const newPort = await input({
16879
16985
  message: "API port:",
16880
- default: String(api.port),
16986
+ default: String(currentPort),
16881
16987
  validate: (v) => {
16882
16988
  const n = Number(v.trim());
16883
16989
  if (!Number.isInteger(n) || n < 1 || n > 65535) return "Must be a valid port (1-65535)";
@@ -16886,24 +16992,24 @@ async function editApi(config, updates, settingsManager) {
16886
16992
  });
16887
16993
  if (settingsManager) {
16888
16994
  await settingsManager.updatePluginSettings("@openacp/api-server", { port: Number(newPort.trim()) });
16889
- } else {
16890
- updates.api = { port: Number(newPort.trim()) };
16891
16995
  }
16892
16996
  console.log(ok(`API port set to ${newPort.trim()}`));
16893
16997
  }
16894
16998
  async function editTunnel(config, updates, settingsManager) {
16895
- let tunnel = config.tunnel ?? { enabled: false, port: 3100, provider: "cloudflare", options: {}, storeTtlMinutes: 60, auth: { enabled: false } };
16896
- if (settingsManager) {
16897
- const ps = await settingsManager.loadSettings("@openacp/tunnel");
16898
- if (Object.keys(ps).length > 0) {
16899
- tunnel = { ...tunnel, ...ps };
16900
- }
16901
- }
16902
- const currentUpdates = updates.tunnel ?? {};
16903
- 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;
16904
17010
  console.log(header("Tunnel"));
16905
17011
  console.log(` Enabled : ${getVal("enabled", false) ? ok("yes") : dim("no")}`);
16906
- console.log(` Provider : ${c.bold}${getVal("provider", "cloudflare")}${c.reset}`);
17012
+ console.log(` Provider : ${getVal("provider", "openacp")}`);
16907
17013
  console.log(` Port : ${getVal("port", 3100)}`);
16908
17014
  const authEnabled = getVal("auth", { enabled: false }).enabled;
16909
17015
  console.log(` Auth : ${authEnabled ? ok("enabled") : dim("disabled")}`);
@@ -16921,8 +17027,6 @@ async function editTunnel(config, updates, settingsManager) {
16921
17027
  ]
16922
17028
  });
16923
17029
  if (choice === "back") break;
16924
- if (!updates.tunnel) updates.tunnel = { ...tunnel };
16925
- const tun = updates.tunnel;
16926
17030
  if (choice === "toggle") {
16927
17031
  const current = getVal("enabled", false);
16928
17032
  if (settingsManager) {
@@ -16935,7 +17039,8 @@ async function editTunnel(config, updates, settingsManager) {
16935
17039
  const provider = await select3({
16936
17040
  message: "Select tunnel provider:",
16937
17041
  choices: [
16938
- { name: "Cloudflare (default)", value: "cloudflare" },
17042
+ { name: "OpenACP (managed)", value: "openacp" },
17043
+ { name: "Cloudflare", value: "cloudflare" },
16939
17044
  { name: "ngrok", value: "ngrok" },
16940
17045
  { name: "bore", value: "bore" },
16941
17046
  { name: "Tailscale Funnel", value: "tailscale" }
@@ -16965,7 +17070,7 @@ async function editTunnel(config, updates, settingsManager) {
16965
17070
  console.log(ok(`Tunnel port set to ${val.trim()}`));
16966
17071
  }
16967
17072
  if (choice === "options") {
16968
- const provider = getVal("provider", "cloudflare");
17073
+ const provider = getVal("provider", "openacp");
16969
17074
  const currentOptions = getVal("options", {});
16970
17075
  await editProviderOptions(provider, currentOptions, tun);
16971
17076
  if (settingsManager) {
@@ -16975,20 +17080,21 @@ async function editTunnel(config, updates, settingsManager) {
16975
17080
  if (choice === "auth") {
16976
17081
  const currentAuth = getVal("auth", { enabled: false });
16977
17082
  if (currentAuth.enabled) {
16978
- tun.auth = { enabled: false };
16979
17083
  if (settingsManager) {
16980
17084
  await settingsManager.updatePluginSettings("@openacp/tunnel", { auth: { enabled: false } });
16981
17085
  }
17086
+ tun.auth = { enabled: false };
16982
17087
  console.log(ok("Tunnel auth disabled"));
16983
17088
  } else {
16984
17089
  const token = await input({
16985
17090
  message: "Auth token (leave empty to auto-generate):",
16986
17091
  default: ""
16987
17092
  });
16988
- tun.auth = token.trim() ? { enabled: true, token: token.trim() } : { enabled: true };
17093
+ const newAuth = token.trim() ? { enabled: true, token: token.trim() } : { enabled: true };
16989
17094
  if (settingsManager) {
16990
- await settingsManager.updatePluginSettings("@openacp/tunnel", { auth: tun.auth });
17095
+ await settingsManager.updatePluginSettings("@openacp/tunnel", { auth: newAuth });
16991
17096
  }
17097
+ tun.auth = newAuth;
16992
17098
  console.log(ok("Tunnel auth enabled"));
16993
17099
  }
16994
17100
  }
@@ -17116,17 +17222,17 @@ ${c.cyan}${c.bold}OpenACP Config Editor${c.reset}`);
17116
17222
  async function sendConfigViaApi(port, updates) {
17117
17223
  const { apiCall: call } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
17118
17224
  const paths = flattenToPaths(updates);
17119
- for (const { path: path35, value } of paths) {
17225
+ for (const { path: path34, value } of paths) {
17120
17226
  const res = await call(port, "/api/config", {
17121
17227
  method: "PATCH",
17122
17228
  headers: { "Content-Type": "application/json" },
17123
- body: JSON.stringify({ path: path35, value })
17229
+ body: JSON.stringify({ path: path34, value })
17124
17230
  });
17125
17231
  const data = await res.json();
17126
17232
  if (!res.ok) {
17127
- console.log(warn(`Failed to update ${path35}: ${data.error}`));
17233
+ console.log(warn(`Failed to update ${path34}: ${data.error}`));
17128
17234
  } else if (data.needsRestart) {
17129
- console.log(warn(`${path35} updated \u2014 restart required`));
17235
+ console.log(warn(`${path34} updated \u2014 restart required`));
17130
17236
  }
17131
17237
  }
17132
17238
  }
@@ -17146,30 +17252,30 @@ function flattenToPaths(obj, prefix = "") {
17146
17252
  // src/cli/daemon.ts
17147
17253
  init_config();
17148
17254
  import { spawn as spawn3 } from "child_process";
17149
- import * as fs30 from "fs";
17150
- import * as path30 from "path";
17255
+ import * as fs29 from "fs";
17256
+ import * as path29 from "path";
17151
17257
  import * as os14 from "os";
17152
- var DEFAULT_ROOT2 = path30.join(os14.homedir(), ".openacp");
17258
+ var DEFAULT_ROOT2 = path29.join(os14.homedir(), ".openacp");
17153
17259
  function getPidPath(root) {
17154
17260
  const base = root ?? DEFAULT_ROOT2;
17155
- return path30.join(base, "openacp.pid");
17261
+ return path29.join(base, "openacp.pid");
17156
17262
  }
17157
17263
  function getLogDir(root) {
17158
17264
  const base = root ?? DEFAULT_ROOT2;
17159
- return path30.join(base, "logs");
17265
+ return path29.join(base, "logs");
17160
17266
  }
17161
17267
  function getRunningMarker(root) {
17162
17268
  const base = root ?? DEFAULT_ROOT2;
17163
- return path30.join(base, "running");
17269
+ return path29.join(base, "running");
17164
17270
  }
17165
17271
  function writePidFile(pidPath, pid) {
17166
- const dir = path30.dirname(pidPath);
17167
- fs30.mkdirSync(dir, { recursive: true });
17168
- fs30.writeFileSync(pidPath, String(pid));
17272
+ const dir = path29.dirname(pidPath);
17273
+ fs29.mkdirSync(dir, { recursive: true });
17274
+ fs29.writeFileSync(pidPath, String(pid));
17169
17275
  }
17170
17276
  function readPidFile(pidPath) {
17171
17277
  try {
17172
- const content = fs30.readFileSync(pidPath, "utf-8").trim();
17278
+ const content = fs29.readFileSync(pidPath, "utf-8").trim();
17173
17279
  const pid = parseInt(content, 10);
17174
17280
  return isNaN(pid) ? null : pid;
17175
17281
  } catch {
@@ -17178,7 +17284,7 @@ function readPidFile(pidPath) {
17178
17284
  }
17179
17285
  function removePidFile(pidPath) {
17180
17286
  try {
17181
- fs30.unlinkSync(pidPath);
17287
+ fs29.unlinkSync(pidPath);
17182
17288
  } catch {
17183
17289
  }
17184
17290
  }
@@ -17211,12 +17317,12 @@ function startDaemon(pidPath = getPidPath(), logDir2, instanceRoot) {
17211
17317
  return { error: `Already running (PID ${pid})` };
17212
17318
  }
17213
17319
  const resolvedLogDir = logDir2 ? expandHome3(logDir2) : getLogDir(instanceRoot);
17214
- fs30.mkdirSync(resolvedLogDir, { recursive: true });
17215
- const logFile = path30.join(resolvedLogDir, "openacp.log");
17216
- 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]);
17217
17323
  const nodePath = process.execPath;
17218
- const out = fs30.openSync(logFile, "a");
17219
- const err = fs30.openSync(logFile, "a");
17324
+ const out = fs29.openSync(logFile, "a");
17325
+ const err = fs29.openSync(logFile, "a");
17220
17326
  const child = spawn3(nodePath, [cliPath, "--daemon-child"], {
17221
17327
  detached: true,
17222
17328
  stdio: ["ignore", out, err],
@@ -17225,8 +17331,8 @@ function startDaemon(pidPath = getPidPath(), logDir2, instanceRoot) {
17225
17331
  ...instanceRoot ? { OPENACP_INSTANCE_ROOT: instanceRoot } : {}
17226
17332
  }
17227
17333
  });
17228
- fs30.closeSync(out);
17229
- fs30.closeSync(err);
17334
+ fs29.closeSync(out);
17335
+ fs29.closeSync(err);
17230
17336
  if (!child.pid) {
17231
17337
  return { error: "Failed to spawn daemon process" };
17232
17338
  }
@@ -17297,12 +17403,12 @@ async function stopDaemon(pidPath = getPidPath(), instanceRoot) {
17297
17403
  }
17298
17404
  function markRunning(root) {
17299
17405
  const marker = getRunningMarker(root);
17300
- fs30.mkdirSync(path30.dirname(marker), { recursive: true });
17301
- fs30.writeFileSync(marker, "");
17406
+ fs29.mkdirSync(path29.dirname(marker), { recursive: true });
17407
+ fs29.writeFileSync(marker, "");
17302
17408
  }
17303
17409
  function clearRunning(root) {
17304
17410
  try {
17305
- fs30.unlinkSync(getRunningMarker(root));
17411
+ fs29.unlinkSync(getRunningMarker(root));
17306
17412
  } catch {
17307
17413
  }
17308
17414
  }
@@ -17527,6 +17633,9 @@ var Draft = class {
17527
17633
  } finally {
17528
17634
  this.firstFlushPending = false;
17529
17635
  }
17636
+ if (this.buffer !== snapshot) {
17637
+ return this.flush();
17638
+ }
17530
17639
  }
17531
17640
  };
17532
17641
  var DraftManager = class {
@@ -17804,8 +17913,8 @@ Configure via \`security.sessionTimeoutMinutes\` in config.
17804
17913
  3. Copy and run it in your terminal \u2014 the session continues there with full conversation history
17805
17914
 
17806
17915
  ### Terminal \u2192 Chat
17807
- 1. First time: run \`openacp integrate claude\` to install the handoff skill (one-time setup)
17808
- 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
17809
17918
  3. The session appears as a new topic/thread and you can continue chatting there
17810
17919
 
17811
17920
  ### How it works