iosm-cli 0.2.11 → 0.2.13

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +6 -3
  3. package/dist/core/agent-session.d.ts +2 -0
  4. package/dist/core/agent-session.d.ts.map +1 -1
  5. package/dist/core/agent-session.js +80 -1
  6. package/dist/core/agent-session.js.map +1 -1
  7. package/dist/core/background-processes.d.ts +11 -0
  8. package/dist/core/background-processes.d.ts.map +1 -1
  9. package/dist/core/background-processes.js +115 -9
  10. package/dist/core/background-processes.js.map +1 -1
  11. package/dist/core/openrouter-model-catalog.d.ts +9 -0
  12. package/dist/core/openrouter-model-catalog.d.ts.map +1 -0
  13. package/dist/core/openrouter-model-catalog.js +139 -0
  14. package/dist/core/openrouter-model-catalog.js.map +1 -0
  15. package/dist/core/settings-manager.d.ts +2 -0
  16. package/dist/core/settings-manager.d.ts.map +1 -1
  17. package/dist/core/settings-manager.js +6 -0
  18. package/dist/core/settings-manager.js.map +1 -1
  19. package/dist/core/slash-commands.d.ts.map +1 -1
  20. package/dist/core/slash-commands.js +9 -1
  21. package/dist/core/slash-commands.js.map +1 -1
  22. package/dist/core/system-prompt.d.ts.map +1 -1
  23. package/dist/core/system-prompt.js +4 -0
  24. package/dist/core/system-prompt.js.map +1 -1
  25. package/dist/core/tools/task.js +1 -1
  26. package/dist/core/tools/task.js.map +1 -1
  27. package/dist/core/usage-cost.d.ts +4 -0
  28. package/dist/core/usage-cost.d.ts.map +1 -0
  29. package/dist/core/usage-cost.js +28 -0
  30. package/dist/core/usage-cost.js.map +1 -0
  31. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  32. package/dist/modes/interactive/components/footer.js +7 -5
  33. package/dist/modes/interactive/components/footer.js.map +1 -1
  34. package/dist/modes/interactive/interactive-mode.d.ts +19 -0
  35. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  36. package/dist/modes/interactive/interactive-mode.js +646 -37
  37. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  38. package/docs/cli-reference.md +11 -2
  39. package/docs/configuration.md +6 -3
  40. package/docs/interactive-mode.md +16 -2
  41. package/docs/sessions-traces-export.md +2 -2
  42. package/package.json +1 -1
@@ -19,6 +19,7 @@ import { MAX_ORCHESTRATION_AGENTS, MAX_ORCHESTRATION_PARALLEL, MAX_SUBAGENT_DELE
19
19
  import { loadModelsDevProviderCatalog, } from "../../core/models-dev-provider-catalog.js";
20
20
  import { ModelRegistry } from "../../core/model-registry.js";
21
21
  import { MODELS_DEV_PROVIDERS } from "../../core/models-dev-providers.js";
22
+ import { loadOpenRouterProviderConfig } from "../../core/openrouter-model-catalog.js";
22
23
  import { resolveModelScope } from "../../core/model-resolver.js";
23
24
  import { getMcpCommandHelp, parseMcpAddCommand, parseMcpTargetCommand, } from "../../core/mcp/index.js";
24
25
  import { addMemoryEntry, getMemoryFilePath, readMemoryEntries, removeMemoryEntry, updateMemoryEntry, } from "../../core/memory.js";
@@ -28,12 +29,13 @@ import { buildProjectIndex, collectChangedFilesSince, ensureProjectIndex, loadPr
28
29
  import { SingularService, } from "../../core/singular.js";
29
30
  import { getDefaultSemanticSearchConfig, getSemanticConfigPath, getSemanticIndexDir, isLikelyEmbeddingModelId, listOllamaLocalModels, listOpenRouterEmbeddingModels, loadMergedSemanticConfig, readScopedSemanticConfig, SemanticConfigMissingError, SemanticIndexRequiredError, SemanticRebuildRequiredError, SemanticSearchRuntime, upsertScopedSemanticSearchConfig, } from "../../core/semantic/index.js";
30
31
  import { DefaultResourceLoader } from "../../core/resource-loader.js";
32
+ import { DefaultPackageManager } from "../../core/package-manager.js";
31
33
  import { createAgentSession } from "../../core/sdk.js";
32
34
  import { createTeamRun, getTeamRun, listTeamRuns } from "../../core/agent-teams.js";
33
35
  import { buildSwarmPlanFromSingular, buildSwarmPlanFromTask, runSwarmScheduler, SwarmStateStore, } from "../../core/swarm/index.js";
34
36
  import { loadCustomSubagents, resolveCustomSubagentReference, } from "../../core/subagents.js";
35
37
  import { getSubagentRun, listSubagentRuns } from "../../core/subagent-runs.js";
36
- import { getBackgroundProcess, listBackgroundProcesses, readBackgroundProcessLogTail, stopBackgroundProcess, } from "../../core/background-processes.js";
38
+ import { getBackgroundProcess, listBackgroundProcesses, pruneBackgroundProcesses, readBackgroundProcessLogTail, stopBackgroundProcess, } from "../../core/background-processes.js";
37
39
  import { SessionManager } from "../../core/session-manager.js";
38
40
  import { SettingsManager } from "../../core/settings-manager.js";
39
41
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
@@ -1005,6 +1007,7 @@ export class InteractiveMode {
1005
1007
  },
1006
1008
  ]));
1007
1009
  this.modelsDevProviderCatalogRefreshPromise = undefined;
1010
+ this.openRouterModelCatalogRefreshPromise = undefined;
1008
1011
  // Custom header from extension (undefined = use built-in header)
1009
1012
  this.customHeader = undefined;
1010
1013
  // Active selector shown in editor container (used to restore UI after temporary dialogs)
@@ -3326,6 +3329,11 @@ export class InteractiveMode {
3326
3329
  await this.handleBackgroundProcessesSlashCommand(text);
3327
3330
  return;
3328
3331
  }
3332
+ if (text === "/extensions" || text.startsWith("/extensions ") || text === "/ext" || text.startsWith("/ext ")) {
3333
+ this.editor.setText("");
3334
+ await this.handleExtensionsSlashCommand(text);
3335
+ return;
3336
+ }
3329
3337
  if (text === "/team-runs" || text.startsWith("/team-runs ")) {
3330
3338
  this.editor.setText("");
3331
3339
  this.handleTeamRunsSlashCommand(text);
@@ -6506,6 +6514,35 @@ export class InteractiveMode {
6506
6514
  return false;
6507
6515
  }
6508
6516
  }
6517
+ async hydrateOpenRouterModelsFromApi(options) {
6518
+ const forceRefresh = options?.forceRefresh === true;
6519
+ if (!forceRefresh && this.hasRegisteredProviderModels(OPENROUTER_PROVIDER_ID))
6520
+ return true;
6521
+ if (forceRefresh && this.isProviderConfiguredInModelsJson(OPENROUTER_PROVIDER_ID)) {
6522
+ return this.hasRegisteredProviderModels(OPENROUTER_PROVIDER_ID);
6523
+ }
6524
+ if (this.openRouterModelCatalogRefreshPromise) {
6525
+ return this.openRouterModelCatalogRefreshPromise;
6526
+ }
6527
+ this.openRouterModelCatalogRefreshPromise = (async () => {
6528
+ const apiKey = await this.session.modelRegistry.authStorage
6529
+ .getApiKey(OPENROUTER_PROVIDER_ID)
6530
+ .catch(() => undefined);
6531
+ const config = await loadOpenRouterProviderConfig({ apiKey });
6532
+ if (!config || !config.models || config.models.length === 0)
6533
+ return false;
6534
+ try {
6535
+ this.session.modelRegistry.registerProvider(OPENROUTER_PROVIDER_ID, config);
6536
+ return this.hasRegisteredProviderModels(OPENROUTER_PROVIDER_ID);
6537
+ }
6538
+ catch {
6539
+ return false;
6540
+ }
6541
+ })().finally(() => {
6542
+ this.openRouterModelCatalogRefreshPromise = undefined;
6543
+ });
6544
+ return this.openRouterModelCatalogRefreshPromise;
6545
+ }
6509
6546
  async hydrateProviderModelsFromModelsDev(providerId, options) {
6510
6547
  const forceRefresh = options?.forceRefresh === true;
6511
6548
  if (!forceRefresh && this.hasRegisteredProviderModels(providerId))
@@ -6513,6 +6550,11 @@ export class InteractiveMode {
6513
6550
  if (forceRefresh && this.isProviderConfiguredInModelsJson(providerId)) {
6514
6551
  return this.hasRegisteredProviderModels(providerId);
6515
6552
  }
6553
+ if (providerId === OPENROUTER_PROVIDER_ID) {
6554
+ const hydratedFromOpenRouter = await this.hydrateOpenRouterModelsFromApi({ forceRefresh });
6555
+ if (hydratedFromOpenRouter)
6556
+ return true;
6557
+ }
6516
6558
  if (!options?.skipCatalogRefresh) {
6517
6559
  await this.refreshModelsDevProviderCatalog();
6518
6560
  }
@@ -6531,8 +6573,11 @@ export class InteractiveMode {
6531
6573
  }
6532
6574
  }
6533
6575
  async hydrateMissingProviderModelsForSavedAuth(options) {
6534
- const savedProviders = this.session.modelRegistry.authStorage.list();
6535
- if (savedProviders.length === 0)
6576
+ const savedProviders = new Set(this.session.modelRegistry.authStorage.list());
6577
+ if (this.session.modelRegistry.authStorage.hasAuth(OPENROUTER_PROVIDER_ID)) {
6578
+ savedProviders.add(OPENROUTER_PROVIDER_ID);
6579
+ }
6580
+ if (savedProviders.size === 0)
6536
6581
  return;
6537
6582
  await this.refreshModelsDevProviderCatalog();
6538
6583
  const forceRefresh = options?.forceRefresh === true;
@@ -12461,54 +12506,273 @@ export class InteractiveMode {
12461
12506
  source: "interactive",
12462
12507
  });
12463
12508
  }
12509
+ getBackgroundCommandUsage() {
12510
+ return "/bg [list|running|done|error|terminated|unknown] [limit: 1..200] | /bg status [id] | /bg logs [id] [lines: 1..1000] | /bg stop [id] | /bg stop-all | /bg prune [hours: 1..2160]";
12511
+ }
12512
+ canUseInteractiveSelectors() {
12513
+ return !!this.ui && !!this.editorContainer;
12514
+ }
12515
+ getBackgroundStatusWeight(status) {
12516
+ switch (status) {
12517
+ case "running":
12518
+ return 0;
12519
+ case "unknown":
12520
+ return 1;
12521
+ case "error":
12522
+ return 2;
12523
+ case "done":
12524
+ return 3;
12525
+ case "terminated":
12526
+ return 4;
12527
+ default:
12528
+ return 5;
12529
+ }
12530
+ }
12531
+ formatBackgroundStatusLabel(status) {
12532
+ switch (status) {
12533
+ case "running":
12534
+ return "RUNNING";
12535
+ case "done":
12536
+ return "DONE";
12537
+ case "error":
12538
+ return "ERROR";
12539
+ case "terminated":
12540
+ return "TERMINATED";
12541
+ case "unknown":
12542
+ return "UNKNOWN";
12543
+ default:
12544
+ return "UNKNOWN";
12545
+ }
12546
+ }
12547
+ formatRelativeTime(timestamp) {
12548
+ if (!timestamp)
12549
+ return "-";
12550
+ const parsed = Date.parse(timestamp);
12551
+ if (!Number.isFinite(parsed))
12552
+ return timestamp;
12553
+ const diffMs = Date.now() - parsed;
12554
+ if (diffMs < 0)
12555
+ return "just now";
12556
+ const diffSec = Math.floor(diffMs / 1000);
12557
+ if (diffSec < 60)
12558
+ return `${Math.max(1, diffSec)}s ago`;
12559
+ const diffMin = Math.floor(diffSec / 60);
12560
+ if (diffMin < 60)
12561
+ return `${diffMin}m ago`;
12562
+ const diffHours = Math.floor(diffMin / 60);
12563
+ if (diffHours < 24)
12564
+ return `${diffHours}h ago`;
12565
+ const diffDays = Math.floor(diffHours / 24);
12566
+ return `${diffDays}d ago`;
12567
+ }
12568
+ formatDurationMs(startedAt, finishedAt) {
12569
+ if (!startedAt)
12570
+ return "-";
12571
+ const started = Date.parse(startedAt);
12572
+ if (!Number.isFinite(started))
12573
+ return "-";
12574
+ const end = finishedAt ? Date.parse(finishedAt) : Date.now();
12575
+ if (!Number.isFinite(end) || end < started)
12576
+ return "-";
12577
+ const diffSec = Math.max(0, Math.floor((end - started) / 1000));
12578
+ const hours = Math.floor(diffSec / 3600);
12579
+ const minutes = Math.floor((diffSec % 3600) / 60);
12580
+ const seconds = diffSec % 60;
12581
+ if (hours > 0)
12582
+ return `${hours}h ${minutes}m`;
12583
+ if (minutes > 0)
12584
+ return `${minutes}m ${seconds}s`;
12585
+ return `${seconds}s`;
12586
+ }
12587
+ sortBackgroundRecords(records) {
12588
+ return [...records].sort((left, right) => {
12589
+ const byStatus = this.getBackgroundStatusWeight(left.status) - this.getBackgroundStatusWeight(right.status);
12590
+ if (byStatus !== 0)
12591
+ return byStatus;
12592
+ return right.createdAt.localeCompare(left.createdAt);
12593
+ });
12594
+ }
12464
12595
  formatBackgroundProcessOptionLabel(record, index) {
12465
- const status = `status=${record.status}`;
12466
- const pid = `pid=${record.pid}`;
12467
- const created = record.createdAt ? `created=${record.createdAt}` : "";
12468
- const commandPreview = record.command.length > 64 ? `${record.command.slice(0, 61)}...` : record.command;
12469
- return `${index + 1}. ${record.id} · ${status} · ${pid}${created ? ` · ${created}` : ""} · ${commandPreview}`;
12470
- }
12471
- async pickBackgroundProcessId(cwd, actionLabel) {
12472
- const records = listBackgroundProcesses(cwd, 30);
12596
+ const statusLabel = this.formatBackgroundStatusLabel(record.status);
12597
+ const age = this.formatRelativeTime(record.createdAt);
12598
+ const runtime = this.formatDurationMs(record.startedAt, record.finishedAt);
12599
+ const source = record.source ?? "-";
12600
+ const commandPreview = record.command.length > 72 ? `${record.command.slice(0, 69)}...` : record.command;
12601
+ const stopFlag = record.requestedStopAt ? " · stop requested" : "";
12602
+ return `${index + 1}. [${statusLabel}] ${record.id} · pid=${record.pid} · age=${age} · runtime=${runtime} · source=${source}${stopFlag}\n ${commandPreview}`;
12603
+ }
12604
+ buildBackgroundProcessesReport(records) {
12605
+ const counts = {
12606
+ running: 0,
12607
+ done: 0,
12608
+ error: 0,
12609
+ terminated: 0,
12610
+ unknown: 0,
12611
+ };
12612
+ for (const record of records) {
12613
+ counts[record.status] += 1;
12614
+ }
12615
+ const header = `Summary: total=${records.length} · running=${counts.running} · done=${counts.done} · error=${counts.error} · terminated=${counts.terminated} · unknown=${counts.unknown}`;
12616
+ const items = records.map((record, index) => this.formatBackgroundProcessOptionLabel(record, index));
12617
+ const hints = [
12618
+ "Quick actions:",
12619
+ "- /bg status <id>",
12620
+ "- /bg logs <id> [lines]",
12621
+ "- /bg stop <id>",
12622
+ "- /bg stop-all",
12623
+ "- /bg prune [hours]",
12624
+ ];
12625
+ return [header, "", ...items, "", ...hints].join("\n");
12626
+ }
12627
+ async runBackgroundMenu(cwd) {
12628
+ if (!this.canUseInteractiveSelectors())
12629
+ return false;
12630
+ const records = this.sortBackgroundRecords(listBackgroundProcesses(cwd, 60));
12631
+ const runningCount = records.filter((record) => record.status === "running").length;
12632
+ const selected = await this.showExtensionSelector(`/bg menu · running=${runningCount} · total=${records.length}`, [
12633
+ "List all processes",
12634
+ "List running only",
12635
+ "Show process status",
12636
+ "Show process logs",
12637
+ "Stop process",
12638
+ "Stop all running processes",
12639
+ "Prune old completed records",
12640
+ "Help",
12641
+ "Cancel",
12642
+ ]);
12643
+ if (!selected || selected === "Cancel") {
12644
+ this.showStatus("/bg menu cancelled.");
12645
+ return true;
12646
+ }
12647
+ if (selected === "List all processes") {
12648
+ await this.handleBackgroundProcessesSlashCommand("/bg list");
12649
+ return true;
12650
+ }
12651
+ if (selected === "List running only") {
12652
+ await this.handleBackgroundProcessesSlashCommand("/bg running");
12653
+ return true;
12654
+ }
12655
+ if (selected === "Show process status") {
12656
+ await this.handleBackgroundProcessesSlashCommand("/bg status");
12657
+ return true;
12658
+ }
12659
+ if (selected === "Show process logs") {
12660
+ await this.handleBackgroundProcessesSlashCommand("/bg logs");
12661
+ return true;
12662
+ }
12663
+ if (selected === "Stop process") {
12664
+ await this.handleBackgroundProcessesSlashCommand("/bg stop");
12665
+ return true;
12666
+ }
12667
+ if (selected === "Stop all running processes") {
12668
+ await this.handleBackgroundProcessesSlashCommand("/bg stop-all");
12669
+ return true;
12670
+ }
12671
+ if (selected === "Prune old completed records") {
12672
+ await this.handleBackgroundProcessesSlashCommand("/bg prune");
12673
+ return true;
12674
+ }
12675
+ if (selected === "Help") {
12676
+ await this.handleBackgroundProcessesSlashCommand("/bg help");
12677
+ return true;
12678
+ }
12679
+ return true;
12680
+ }
12681
+ async pickBackgroundProcessId(cwd, actionLabel, options) {
12682
+ const limit = options?.limit ?? 50;
12683
+ const records = this.sortBackgroundRecords(listBackgroundProcesses(cwd, limit));
12473
12684
  if (records.length === 0) {
12474
12685
  this.showStatus("No background processes found.");
12475
12686
  return undefined;
12476
12687
  }
12477
- const options = records.map((record, index) => this.formatBackgroundProcessOptionLabel(record, index));
12478
- const selected = await this.showExtensionSelector(`/bg ${actionLabel}: select process`, options);
12688
+ const preferredStatuses = options?.preferredStatuses;
12689
+ let filtered = records;
12690
+ if (preferredStatuses && preferredStatuses.length > 0) {
12691
+ const allowed = new Set(preferredStatuses);
12692
+ const preferred = records.filter((record) => allowed.has(record.status));
12693
+ if (preferred.length > 0)
12694
+ filtered = preferred;
12695
+ }
12696
+ const pickerOptions = filtered.map((record, index) => this.formatBackgroundProcessOptionLabel(record, index));
12697
+ const selected = await this.showExtensionSelector(`/bg ${actionLabel}: select process`, pickerOptions);
12479
12698
  if (!selected) {
12480
12699
  this.showStatus(`/bg ${actionLabel} cancelled.`);
12481
12700
  return undefined;
12482
12701
  }
12483
- const selectedIndex = options.indexOf(selected);
12484
- return selectedIndex >= 0 ? records[selectedIndex]?.id : undefined;
12702
+ const selectedIndex = pickerOptions.indexOf(selected);
12703
+ return selectedIndex >= 0 ? filtered[selectedIndex]?.id : undefined;
12704
+ }
12705
+ async stopAllBackgroundProcesses(cwd) {
12706
+ const runningRecords = this.sortBackgroundRecords(listBackgroundProcesses(cwd, 200)).filter((record) => record.status === "running");
12707
+ if (runningRecords.length === 0) {
12708
+ this.showStatus("No running background processes found.");
12709
+ return;
12710
+ }
12711
+ if (this.canUseInteractiveSelectors()) {
12712
+ const confirmed = await this.showExtensionConfirm("Stop all background processes?", `Send stop signal to ${runningRecords.length} running process(es).`);
12713
+ if (!confirmed) {
12714
+ this.showStatus("/bg stop-all cancelled.");
12715
+ return;
12716
+ }
12717
+ }
12718
+ const updatedRecords = [];
12719
+ for (const record of runningRecords) {
12720
+ const updated = stopBackgroundProcess(cwd, record.id);
12721
+ if (updated)
12722
+ updatedRecords.push(updated);
12723
+ }
12724
+ const stillRunning = updatedRecords.filter((record) => record.status === "running");
12725
+ const lines = [
12726
+ `Stop-all requested for ${runningRecords.length} process(es).`,
12727
+ `Still running: ${stillRunning.length}`,
12728
+ "",
12729
+ ...updatedRecords.map((record, index) => `${index + 1}. ${record.id} · status=${record.status} · requested_stop=${record.requestedStopAt ?? "-"}`),
12730
+ ];
12731
+ this.showCommandTextBlock("Background Stop-All", lines.join("\n"));
12485
12732
  }
12486
12733
  async handleBackgroundProcessesSlashCommand(text) {
12487
12734
  const args = this.parseSlashArgs(text).slice(1);
12488
12735
  const cwd = this.sessionManager.getCwd();
12736
+ const usage = this.getBackgroundCommandUsage();
12737
+ if (args.length === 0) {
12738
+ const handledByMenu = await this.runBackgroundMenu(cwd);
12739
+ if (handledByMenu)
12740
+ return;
12741
+ }
12489
12742
  const firstArg = (args[0] ?? "").toLowerCase();
12490
12743
  const subcommand = firstArg || "list";
12491
- if (subcommand === "list" || /^\d+$/.test(subcommand)) {
12492
- const limitRaw = subcommand === "list" ? args[1] : subcommand;
12744
+ const listFilters = {
12745
+ list: undefined,
12746
+ running: ["running"],
12747
+ done: ["done"],
12748
+ error: ["error"],
12749
+ failed: ["error"],
12750
+ terminated: ["terminated"],
12751
+ unknown: ["unknown"],
12752
+ };
12753
+ const hasListFilter = Object.prototype.hasOwnProperty.call(listFilters, subcommand);
12754
+ if (hasListFilter || /^\d+$/.test(subcommand)) {
12755
+ const limitRaw = subcommand === "list" ? args[1] : hasListFilter ? args[1] : subcommand;
12493
12756
  const limit = limitRaw ? Number.parseInt(limitRaw, 10) : 20;
12494
12757
  if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
12495
- this.showWarning("Usage: /bg [list [limit: 1..200]] | /bg status [id] | /bg logs [id] [lines] | /bg stop [id]");
12758
+ this.showWarning(`Usage: ${usage}`);
12496
12759
  return;
12497
12760
  }
12498
- const records = listBackgroundProcesses(cwd, limit);
12499
- if (records.length === 0) {
12500
- this.showStatus("No background processes found.");
12761
+ const records = this.sortBackgroundRecords(listBackgroundProcesses(cwd, limit));
12762
+ const statusFilter = hasListFilter ? listFilters[subcommand] : undefined;
12763
+ const filtered = statusFilter && statusFilter.length > 0
12764
+ ? records.filter((record) => statusFilter.includes(record.status))
12765
+ : records;
12766
+ if (filtered.length === 0) {
12767
+ if (statusFilter && statusFilter.length > 0) {
12768
+ this.showStatus(`No background processes found for "${subcommand}" filter.`);
12769
+ }
12770
+ else {
12771
+ this.showStatus("No background processes found.");
12772
+ }
12501
12773
  return;
12502
12774
  }
12503
- const lines = records.map((record, index) => {
12504
- const status = `status=${record.status}`;
12505
- const pid = `pid=${record.pid}`;
12506
- const created = record.createdAt ? ` · ${record.createdAt}` : "";
12507
- const finished = record.finishedAt ? ` · finished=${record.finishedAt}` : "";
12508
- const exitCode = typeof record.exitCode === "number" ? ` · exit=${record.exitCode}` : "";
12509
- return `${index + 1}. ${record.id} · ${status} · ${pid}${created}${finished}${exitCode}\n ${record.command}`;
12510
- });
12511
- this.showCommandTextBlock("Background Processes", lines.join("\n"));
12775
+ this.showCommandTextBlock("Background Processes", this.buildBackgroundProcessesReport(filtered));
12512
12776
  return;
12513
12777
  }
12514
12778
  if (subcommand === "status") {
@@ -12525,10 +12789,11 @@ export class InteractiveMode {
12525
12789
  }
12526
12790
  const lines = [
12527
12791
  `ID: ${record.id}`,
12528
- `Status: ${record.status}`,
12792
+ `Status: ${this.formatBackgroundStatusLabel(record.status)}${record.requestedStopAt ? " (stop requested)" : ""}`,
12529
12793
  `PID: ${record.pid}`,
12530
- `Created: ${record.createdAt}`,
12794
+ `Created: ${record.createdAt} (${this.formatRelativeTime(record.createdAt)})`,
12531
12795
  `Started: ${record.startedAt}`,
12796
+ `Runtime: ${this.formatDurationMs(record.startedAt, record.finishedAt)}`,
12532
12797
  `Finished: ${record.finishedAt ?? "-"}`,
12533
12798
  `Exit code: ${typeof record.exitCode === "number" ? record.exitCode : "-"}`,
12534
12799
  `Cwd: ${record.cwd}`,
@@ -12537,6 +12802,10 @@ export class InteractiveMode {
12537
12802
  `Status file: ${record.metaPath}`,
12538
12803
  `Log file: ${record.logPath}`,
12539
12804
  "",
12805
+ "Quick actions:",
12806
+ `- /bg logs ${record.id} 120`,
12807
+ `- /bg stop ${record.id}`,
12808
+ "",
12540
12809
  "Command:",
12541
12810
  record.command,
12542
12811
  ];
@@ -12546,7 +12815,9 @@ export class InteractiveMode {
12546
12815
  if (subcommand === "logs") {
12547
12816
  let id = args[1];
12548
12817
  if (!id) {
12549
- id = await this.pickBackgroundProcessId(cwd, "logs");
12818
+ id = await this.pickBackgroundProcessId(cwd, "logs", {
12819
+ preferredStatuses: ["running", "unknown", "error", "done", "terminated"],
12820
+ });
12550
12821
  }
12551
12822
  if (!id)
12552
12823
  return;
@@ -12567,30 +12838,368 @@ export class InteractiveMode {
12567
12838
  return;
12568
12839
  }
12569
12840
  const body = tail.trim().length > 0 ? tail : "(no output yet)";
12570
- this.showCommandTextBlock("Background Logs", [`ID: ${record.id} · status=${record.status} · tail=${tailLines} lines`, "", body].join("\n"));
12841
+ this.showCommandTextBlock("Background Logs", [
12842
+ `ID: ${record.id} · status=${this.formatBackgroundStatusLabel(record.status)} · tail=${tailLines} lines`,
12843
+ `Runtime: ${this.formatDurationMs(record.startedAt, record.finishedAt)} · stop_requested=${record.requestedStopAt ? "yes" : "no"}`,
12844
+ `Command: ${record.command.length > 120 ? `${record.command.slice(0, 117)}...` : record.command}`,
12845
+ "",
12846
+ body,
12847
+ ].join("\n"));
12571
12848
  return;
12572
12849
  }
12573
12850
  if (subcommand === "stop" || subcommand === "kill" || subcommand === "cancel") {
12574
12851
  let id = args[1];
12575
12852
  if (!id) {
12576
- id = await this.pickBackgroundProcessId(cwd, "stop");
12853
+ id = await this.pickBackgroundProcessId(cwd, "stop", {
12854
+ preferredStatuses: ["running", "unknown", "error", "done", "terminated"],
12855
+ });
12577
12856
  }
12578
12857
  if (!id)
12579
12858
  return;
12859
+ const current = getBackgroundProcess(cwd, id);
12860
+ if (!current) {
12861
+ this.showWarning(`Background process not found: ${id}`);
12862
+ return;
12863
+ }
12864
+ if (current.status !== "running") {
12865
+ this.showStatus(`Background process ${current.id} is already ${current.status}.`);
12866
+ return;
12867
+ }
12868
+ if (this.canUseInteractiveSelectors()) {
12869
+ const confirmed = await this.showExtensionConfirm("Stop background process?", `${current.id}\n${current.command.length > 140 ? `${current.command.slice(0, 137)}...` : current.command}`);
12870
+ if (!confirmed) {
12871
+ this.showStatus(`/bg stop cancelled for ${current.id}.`);
12872
+ return;
12873
+ }
12874
+ }
12580
12875
  const record = stopBackgroundProcess(cwd, id);
12581
12876
  if (!record) {
12582
12877
  this.showWarning(`Background process not found: ${id}`);
12583
12878
  return;
12584
12879
  }
12585
12880
  if (record.status === "running") {
12586
- this.showWarning(`Stop signal sent to ${record.id}, but process still appears running.`);
12881
+ this.showWarning(`Stop requested for ${record.id}, waiting for graceful shutdown. Use /bg status ${record.id} to monitor.`);
12587
12882
  }
12588
12883
  else {
12589
12884
  this.showStatus(`Background process ${record.id} stopped (${record.status}).`);
12590
12885
  }
12591
12886
  return;
12592
12887
  }
12593
- this.showWarning("Usage: /bg [list [limit: 1..200]] | /bg status [id] | /bg logs [id] [lines] | /bg stop [id]");
12888
+ if (subcommand === "stop-all" || subcommand === "kill-all" || subcommand === "cancel-all") {
12889
+ await this.stopAllBackgroundProcesses(cwd);
12890
+ return;
12891
+ }
12892
+ if (subcommand === "prune" || subcommand === "cleanup") {
12893
+ const hoursRaw = args[1];
12894
+ const hours = hoursRaw ? Number.parseInt(hoursRaw, 10) : 168;
12895
+ if (!Number.isInteger(hours) || hours < 1 || hours > 2160) {
12896
+ this.showWarning("Usage: /bg prune [hours: 1..2160]");
12897
+ return;
12898
+ }
12899
+ const result = pruneBackgroundProcesses(cwd, { maxAgeHours: hours });
12900
+ const preview = result.removedIds.length > 0 ? `\n${result.removedIds.slice(0, 10).map((id) => `- ${id}`).join("\n")}` : "";
12901
+ const moreSuffix = result.removedIds.length > 10 ? `\n- ... (+${result.removedIds.length - 10} more)` : "";
12902
+ this.showCommandTextBlock("Background Prune", [
12903
+ `Threshold: older than ${result.thresholdHours}h`,
12904
+ `Removed: ${result.removed}`,
12905
+ `Skipped running: ${result.skippedRunning}`,
12906
+ `Skipped recent: ${result.skippedRecent}`,
12907
+ preview ? "" : undefined,
12908
+ preview || undefined,
12909
+ moreSuffix || undefined,
12910
+ ]
12911
+ .filter((line) => typeof line === "string")
12912
+ .join("\n"));
12913
+ return;
12914
+ }
12915
+ if (subcommand === "help") {
12916
+ this.showCommandTextBlock("Background Help", [
12917
+ "Background command usage:",
12918
+ usage,
12919
+ "",
12920
+ "Examples:",
12921
+ "- ! npm run dev &",
12922
+ "- /bg",
12923
+ "- /bg running",
12924
+ "- /bg status bg_123",
12925
+ "- /bg logs bg_123 200",
12926
+ "- /bg stop bg_123",
12927
+ "- /bg stop-all",
12928
+ "- /bg prune 72",
12929
+ ].join("\n"));
12930
+ return;
12931
+ }
12932
+ this.showWarning(`Usage: ${usage}`);
12933
+ }
12934
+ getExtensionsCommandUsage() {
12935
+ return "/extensions (/ext) [list|install <source>|update [source]|remove <source>|enable <target>|disable <target>|help] [--global]";
12936
+ }
12937
+ createInteractivePackageManager(cwd) {
12938
+ return new DefaultPackageManager({
12939
+ cwd,
12940
+ agentDir: getAgentDir(),
12941
+ settingsManager: this.settingsManager,
12942
+ });
12943
+ }
12944
+ normalizeExtensionOverrideTarget(target) {
12945
+ const trimmed = target.trim();
12946
+ if (!trimmed)
12947
+ return "";
12948
+ if (path.isAbsolute(trimmed)) {
12949
+ return path.resolve(trimmed);
12950
+ }
12951
+ let normalized = trimmed.replace(/\\/g, "/");
12952
+ if (normalized.startsWith("./"))
12953
+ normalized = normalized.slice(2);
12954
+ if (!normalized.startsWith("extensions/")) {
12955
+ normalized = `extensions/${normalized}`;
12956
+ }
12957
+ return normalized;
12958
+ }
12959
+ setExtensionOverride(scope, target, enabled) {
12960
+ const normalizedTarget = this.normalizeExtensionOverrideTarget(target);
12961
+ if (!normalizedTarget)
12962
+ return { updated: false, token: undefined };
12963
+ const current = scope === "project"
12964
+ ? [...(this.settingsManager.getProjectSettings().extensions ?? [])]
12965
+ : [...(this.settingsManager.getGlobalSettings().extensions ?? [])];
12966
+ const normalizeEntryTarget = (entry) => {
12967
+ const trimmed = entry.trim();
12968
+ if (trimmed.startsWith("+") || trimmed.startsWith("-")) {
12969
+ return this.normalizeExtensionOverrideTarget(trimmed.slice(1));
12970
+ }
12971
+ return "";
12972
+ };
12973
+ const filtered = current.filter((entry) => normalizeEntryTarget(entry) !== normalizedTarget);
12974
+ const token = `${enabled ? "+" : "-"}${normalizedTarget}`;
12975
+ const next = [...filtered, token];
12976
+ if (JSON.stringify(next) === JSON.stringify(current)) {
12977
+ return { updated: false, token };
12978
+ }
12979
+ if (scope === "project") {
12980
+ this.settingsManager.setProjectExtensionPaths(next);
12981
+ }
12982
+ else {
12983
+ this.settingsManager.setExtensionPaths(next);
12984
+ }
12985
+ return { updated: true, token };
12986
+ }
12987
+ togglePackageExtensionSource(scope, source, enabled) {
12988
+ const settings = scope === "project" ? this.settingsManager.getProjectSettings() : this.settingsManager.getGlobalSettings();
12989
+ const packages = [...(settings.packages ?? [])];
12990
+ const index = packages.findIndex((pkg) => (typeof pkg === "string" ? pkg : pkg.source) === source);
12991
+ if (index < 0) {
12992
+ return { matched: false, changed: false, message: "" };
12993
+ }
12994
+ const entry = packages[index];
12995
+ let nextEntry = entry;
12996
+ if (!enabled) {
12997
+ if (typeof entry === "string") {
12998
+ nextEntry = { source: entry, extensions: [] };
12999
+ }
13000
+ else if (Array.isArray(entry.extensions) && entry.extensions.length === 0) {
13001
+ return { matched: true, changed: false, message: `Extension source already disabled: ${source}` };
13002
+ }
13003
+ else {
13004
+ nextEntry = { ...entry, extensions: [] };
13005
+ }
13006
+ }
13007
+ else if (typeof entry === "string") {
13008
+ return { matched: true, changed: false, message: `Extension source already enabled: ${source}` };
13009
+ }
13010
+ else if (!Array.isArray(entry.extensions) || entry.extensions.length > 0) {
13011
+ return { matched: true, changed: false, message: `Extension source already enabled: ${source}` };
13012
+ }
13013
+ else {
13014
+ const { extensions: _extensions, ...rest } = entry;
13015
+ nextEntry = Object.keys(rest).length === 1 ? rest.source : rest;
13016
+ }
13017
+ packages[index] = nextEntry;
13018
+ if (scope === "project") {
13019
+ this.settingsManager.setProjectPackages(packages);
13020
+ }
13021
+ else {
13022
+ this.settingsManager.setPackages(packages);
13023
+ }
13024
+ return {
13025
+ matched: true,
13026
+ changed: true,
13027
+ message: `${enabled ? "Enabled" : "Disabled"} extension source (${scope}): ${source}`,
13028
+ };
13029
+ }
13030
+ async showExtensionsList(scopeFilter) {
13031
+ const cwd = this.sessionManager.getCwd();
13032
+ const packageManager = this.createInteractivePackageManager(cwd);
13033
+ const resolved = await packageManager.resolve();
13034
+ const packageLines = [];
13035
+ const projectPackages = this.settingsManager.getProjectSettings().packages ?? [];
13036
+ const globalPackages = this.settingsManager.getGlobalSettings().packages ?? [];
13037
+ const includeProject = !scopeFilter || scopeFilter === "project";
13038
+ const includeGlobal = !scopeFilter || scopeFilter === "user";
13039
+ if (includeProject) {
13040
+ packageLines.push("Project package sources:");
13041
+ if (projectPackages.length === 0) {
13042
+ packageLines.push(" (none)");
13043
+ }
13044
+ else {
13045
+ for (const pkg of projectPackages) {
13046
+ const source = typeof pkg === "string" ? pkg : pkg.source;
13047
+ const status = typeof pkg === "object" && Array.isArray(pkg.extensions) && pkg.extensions.length === 0
13048
+ ? "disabled"
13049
+ : "enabled";
13050
+ packageLines.push(` - [${status}] ${source}`);
13051
+ }
13052
+ }
13053
+ }
13054
+ if (includeGlobal) {
13055
+ if (packageLines.length > 0)
13056
+ packageLines.push("");
13057
+ packageLines.push("User package sources:");
13058
+ if (globalPackages.length === 0) {
13059
+ packageLines.push(" (none)");
13060
+ }
13061
+ else {
13062
+ for (const pkg of globalPackages) {
13063
+ const source = typeof pkg === "string" ? pkg : pkg.source;
13064
+ const status = typeof pkg === "object" && Array.isArray(pkg.extensions) && pkg.extensions.length === 0
13065
+ ? "disabled"
13066
+ : "enabled";
13067
+ packageLines.push(` - [${status}] ${source}`);
13068
+ }
13069
+ }
13070
+ }
13071
+ const extensions = resolved.extensions
13072
+ .filter((resource) => !scopeFilter || resource.metadata.scope === scopeFilter)
13073
+ .sort((a, b) => {
13074
+ if (a.metadata.scope !== b.metadata.scope)
13075
+ return a.metadata.scope.localeCompare(b.metadata.scope, "en");
13076
+ return a.path.localeCompare(b.path, "en");
13077
+ });
13078
+ const enabledCount = extensions.filter((resource) => resource.enabled).length;
13079
+ const disabledCount = extensions.length - enabledCount;
13080
+ const extensionLines = extensions.length === 0
13081
+ ? [" (none)"]
13082
+ : extensions.map((resource) => {
13083
+ const relative = path.relative(cwd, resource.path);
13084
+ const displayPath = !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : resource.path;
13085
+ const status = resource.enabled ? "enabled" : "disabled";
13086
+ return ` - [${status}] ${displayPath} · scope=${resource.metadata.scope} · source=${resource.metadata.source}`;
13087
+ });
13088
+ const lines = [
13089
+ `Extension resources: total=${extensions.length} · enabled=${enabledCount} · disabled=${disabledCount}`,
13090
+ "",
13091
+ ...extensionLines,
13092
+ "",
13093
+ ...packageLines,
13094
+ "",
13095
+ "Commands:",
13096
+ "- /extensions install <source> [--global]",
13097
+ "- /extensions update [source]",
13098
+ "- /extensions remove <source> [--global]",
13099
+ "- /extensions disable <source-or-path> [--global]",
13100
+ "- /extensions enable <source-or-path> [--global]",
13101
+ ];
13102
+ this.showCommandTextBlock("Extensions", lines.join("\n"));
13103
+ }
13104
+ async handleExtensionsSlashCommand(text) {
13105
+ const args = this.parseSlashArgs(text).slice(1);
13106
+ const usage = this.getExtensionsCommandUsage();
13107
+ const filteredArgs = args.filter((arg) => arg !== "/extensions" && arg !== "/ext");
13108
+ let scope = "project";
13109
+ const normalizedArgs = [];
13110
+ for (const arg of filteredArgs) {
13111
+ if (arg === "--global" || arg === "-g") {
13112
+ scope = "user";
13113
+ continue;
13114
+ }
13115
+ if (arg === "--project") {
13116
+ scope = "project";
13117
+ continue;
13118
+ }
13119
+ normalizedArgs.push(arg);
13120
+ }
13121
+ const subcommand = (normalizedArgs[0] ?? "list").toLowerCase();
13122
+ const subArgs = normalizedArgs.slice(1);
13123
+ const cwd = this.sessionManager.getCwd();
13124
+ const packageManager = this.createInteractivePackageManager(cwd);
13125
+ if (subcommand === "help") {
13126
+ this.showCommandTextBlock("Extensions Help", [
13127
+ "Extension lifecycle usage:",
13128
+ usage,
13129
+ "",
13130
+ "Examples:",
13131
+ "- /extensions list",
13132
+ "- /extensions list --global",
13133
+ "- /extensions install npm:@org/iosm-extension",
13134
+ "- /extensions install ./local-extension --global",
13135
+ "- /extensions update",
13136
+ "- /extensions remove npm:@org/iosm-extension",
13137
+ "- /extensions disable extensions/my-tool.ts",
13138
+ "- /ext enable npm:@org/iosm-extension --global",
13139
+ ].join("\n"));
13140
+ return;
13141
+ }
13142
+ if (subcommand === "list") {
13143
+ await this.showExtensionsList(scope);
13144
+ return;
13145
+ }
13146
+ if (subcommand === "install") {
13147
+ const source = subArgs[0];
13148
+ if (!source) {
13149
+ this.showWarning(`Usage: ${usage}`);
13150
+ return;
13151
+ }
13152
+ await packageManager.install(source, { local: scope === "project" });
13153
+ packageManager.addSourceToSettings(source, { local: scope === "project" });
13154
+ await this.reloadAfterMemoryMutation(`Installed extension source (${scope}): ${source}`, "Extension source installed but reload failed");
13155
+ return;
13156
+ }
13157
+ if (subcommand === "update") {
13158
+ const source = subArgs[0];
13159
+ await packageManager.update(source);
13160
+ await this.reloadAfterMemoryMutation(source ? `Updated extension source: ${source}` : "Updated extension sources", "Extension sources updated but reload failed");
13161
+ return;
13162
+ }
13163
+ if (subcommand === "remove" || subcommand === "uninstall") {
13164
+ const source = subArgs[0];
13165
+ if (!source) {
13166
+ this.showWarning(`Usage: ${usage}`);
13167
+ return;
13168
+ }
13169
+ await packageManager.remove(source, { local: scope === "project" });
13170
+ const removed = packageManager.removeSourceFromSettings(source, { local: scope === "project" });
13171
+ if (!removed) {
13172
+ this.showWarning(`No matching extension source found in ${scope} scope: ${source}`);
13173
+ return;
13174
+ }
13175
+ await this.reloadAfterMemoryMutation(`Removed extension source (${scope}): ${source}`, "Extension source removed but reload failed");
13176
+ return;
13177
+ }
13178
+ if (subcommand === "enable" || subcommand === "disable") {
13179
+ const target = subArgs[0];
13180
+ if (!target) {
13181
+ this.showWarning(`Usage: ${usage}`);
13182
+ return;
13183
+ }
13184
+ const desiredEnabled = subcommand === "enable";
13185
+ const packageToggle = this.togglePackageExtensionSource(scope, target, desiredEnabled);
13186
+ if (packageToggle.matched) {
13187
+ if (!packageToggle.changed) {
13188
+ this.showStatus(packageToggle.message);
13189
+ return;
13190
+ }
13191
+ await this.reloadAfterMemoryMutation(packageToggle.message, "Extension source toggle applied but reload failed");
13192
+ return;
13193
+ }
13194
+ const override = this.setExtensionOverride(scope, target, desiredEnabled);
13195
+ if (!override.updated) {
13196
+ this.showStatus(`No extension override changes were needed for: ${target}`);
13197
+ return;
13198
+ }
13199
+ await this.reloadAfterMemoryMutation(`${desiredEnabled ? "Enabled" : "Disabled"} extension path (${scope}) via override: ${override.token}`, "Extension override applied but reload failed");
13200
+ return;
13201
+ }
13202
+ this.showWarning(`Usage: ${usage}`);
12594
13203
  }
12595
13204
  handleTeamRunsSlashCommand(text) {
12596
13205
  const args = this.parseSlashArgs(text).slice(1);