iosm-cli 0.1.2 → 0.1.3

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 (83) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +6 -3
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +4 -2
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/agent-profiles.d.ts.map +1 -1
  7. package/dist/core/agent-profiles.js +1 -0
  8. package/dist/core/agent-profiles.js.map +1 -1
  9. package/dist/core/agent-session.d.ts.map +1 -1
  10. package/dist/core/agent-session.js +3 -0
  11. package/dist/core/agent-session.js.map +1 -1
  12. package/dist/core/sdk.d.ts +3 -3
  13. package/dist/core/sdk.d.ts.map +1 -1
  14. package/dist/core/sdk.js +11 -19
  15. package/dist/core/sdk.js.map +1 -1
  16. package/dist/core/semantic/chunking.d.ts +10 -0
  17. package/dist/core/semantic/chunking.d.ts.map +1 -0
  18. package/dist/core/semantic/chunking.js +82 -0
  19. package/dist/core/semantic/chunking.js.map +1 -0
  20. package/dist/core/semantic/cli.d.ts +23 -0
  21. package/dist/core/semantic/cli.d.ts.map +1 -0
  22. package/dist/core/semantic/cli.js +86 -0
  23. package/dist/core/semantic/cli.js.map +1 -0
  24. package/dist/core/semantic/config.d.ts +8 -0
  25. package/dist/core/semantic/config.d.ts.map +1 -0
  26. package/dist/core/semantic/config.js +261 -0
  27. package/dist/core/semantic/config.js.map +1 -0
  28. package/dist/core/semantic/index-store.d.ts +21 -0
  29. package/dist/core/semantic/index-store.d.ts.map +1 -0
  30. package/dist/core/semantic/index-store.js +73 -0
  31. package/dist/core/semantic/index-store.js.map +1 -0
  32. package/dist/core/semantic/index.d.ts +8 -0
  33. package/dist/core/semantic/index.d.ts.map +1 -0
  34. package/dist/core/semantic/index.js +7 -0
  35. package/dist/core/semantic/index.js.map +1 -0
  36. package/dist/core/semantic/providers.d.ts +22 -0
  37. package/dist/core/semantic/providers.d.ts.map +1 -0
  38. package/dist/core/semantic/providers.js +317 -0
  39. package/dist/core/semantic/providers.js.map +1 -0
  40. package/dist/core/semantic/runtime.d.ts +32 -0
  41. package/dist/core/semantic/runtime.d.ts.map +1 -0
  42. package/dist/core/semantic/runtime.js +499 -0
  43. package/dist/core/semantic/runtime.js.map +1 -0
  44. package/dist/core/semantic/types.d.ts +151 -0
  45. package/dist/core/semantic/types.d.ts.map +1 -0
  46. package/dist/core/semantic/types.js +17 -0
  47. package/dist/core/semantic/types.js.map +1 -0
  48. package/dist/core/slash-commands.d.ts.map +1 -1
  49. package/dist/core/slash-commands.js +4 -0
  50. package/dist/core/slash-commands.js.map +1 -1
  51. package/dist/core/system-prompt.d.ts.map +1 -1
  52. package/dist/core/system-prompt.js +19 -3
  53. package/dist/core/system-prompt.js.map +1 -1
  54. package/dist/core/tools/ast-grep.js +1 -1
  55. package/dist/core/tools/ast-grep.js.map +1 -1
  56. package/dist/core/tools/comby.js +1 -1
  57. package/dist/core/tools/comby.js.map +1 -1
  58. package/dist/core/tools/index.d.ts +9 -0
  59. package/dist/core/tools/index.d.ts.map +1 -1
  60. package/dist/core/tools/index.js +6 -0
  61. package/dist/core/tools/index.js.map +1 -1
  62. package/dist/core/tools/rg.js +1 -1
  63. package/dist/core/tools/rg.js.map +1 -1
  64. package/dist/core/tools/semantic-search.d.ts +21 -0
  65. package/dist/core/tools/semantic-search.d.ts.map +1 -0
  66. package/dist/core/tools/semantic-search.js +122 -0
  67. package/dist/core/tools/semantic-search.js.map +1 -0
  68. package/dist/index.d.ts +4 -2
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +3 -2
  71. package/dist/index.js.map +1 -1
  72. package/dist/main.d.ts.map +1 -1
  73. package/dist/main.js +117 -0
  74. package/dist/main.js.map +1 -1
  75. package/dist/modes/interactive/interactive-mode.d.ts +15 -0
  76. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  77. package/dist/modes/interactive/interactive-mode.js +687 -4
  78. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  79. package/docs/cli-reference.md +18 -1
  80. package/docs/configuration.md +74 -2
  81. package/docs/getting-started.md +1 -0
  82. package/docs/interactive-mode.md +5 -1
  83. package/package.json +1 -1
@@ -19,6 +19,7 @@ import { ModelRegistry } from "../../core/model-registry.js";
19
19
  import { resolveModelScope } from "../../core/model-resolver.js";
20
20
  import { getMcpCommandHelp, parseMcpAddCommand, parseMcpTargetCommand, } from "../../core/mcp/index.js";
21
21
  import { addMemoryEntry, getMemoryFilePath, readMemoryEntries, removeMemoryEntry, updateMemoryEntry, } from "../../core/memory.js";
22
+ import { getDefaultSemanticSearchConfig, getSemanticConfigPath, getSemanticIndexDir, isLikelyEmbeddingModelId, listOllamaLocalModels, listOpenRouterEmbeddingModels, loadMergedSemanticConfig, SemanticConfigMissingError, SemanticRebuildRequiredError, SemanticSearchRuntime, upsertScopedSemanticSearchConfig, } from "../../core/semantic/index.js";
22
23
  import { DefaultResourceLoader } from "../../core/resource-loader.js";
23
24
  import { createAgentSession } from "../../core/sdk.js";
24
25
  import { createTeamRun, getTeamRun, listTeamRuns } from "../../core/agent-teams.js";
@@ -797,6 +798,42 @@ export class InteractiveMode {
797
798
  }
798
799
  return null;
799
800
  }
801
+ getSemanticArgumentCompletions(prefix) {
802
+ const subcommands = ["ui", "setup", "status", "index", "rebuild", "query", "help"];
803
+ const queryFlags = ["--top-k"];
804
+ const topKValues = ["5", "8", "10", "20"];
805
+ const hasTrailingSpace = /\\s$/.test(prefix);
806
+ const tokens = this.parseSlashArgs(prefix);
807
+ const first = tokens[0]?.toLowerCase();
808
+ if (!first || (tokens.length === 1 && !hasTrailingSpace)) {
809
+ const query = first ?? "";
810
+ const candidates = subcommands.filter((item) => item.includes(query));
811
+ return candidates.map((item) => ({ value: item, label: item }));
812
+ }
813
+ if (first !== "query") {
814
+ return null;
815
+ }
816
+ const topKIndex = tokens.findIndex((token) => token === "--top-k");
817
+ if (topKIndex >= 0) {
818
+ const currentValue = tokens[topKIndex + 1];
819
+ if (!currentValue) {
820
+ return topKValues.map((value) => ({ value, label: value }));
821
+ }
822
+ return topKValues
823
+ .filter((value) => value.startsWith(currentValue))
824
+ .map((value) => ({ value, label: value }));
825
+ }
826
+ const query = hasTrailingSpace ? "" : (tokens[tokens.length - 1] ?? "");
827
+ if (query.startsWith("--")) {
828
+ return queryFlags
829
+ .filter((flag) => flag.includes(query))
830
+ .map((flag) => ({ value: flag, label: flag }));
831
+ }
832
+ if (hasTrailingSpace) {
833
+ return queryFlags.map((flag) => ({ value: flag, label: flag }));
834
+ }
835
+ return null;
836
+ }
800
837
  setupAutocomplete(fdPath) {
801
838
  // Define commands for autocomplete
802
839
  const builtinCommands = BUILTIN_SLASH_COMMANDS.filter((command) => this.activeProfileName === "iosm" || !IOSM_PROFILE_ONLY_COMMANDS.has(command.name));
@@ -838,6 +875,10 @@ export class InteractiveMode {
838
875
  if (memoryCommand) {
839
876
  memoryCommand.getArgumentCompletions = (prefix) => this.getMemoryArgumentCompletions(prefix);
840
877
  }
878
+ const semanticCommand = slashCommands.find((command) => command.name === "semantic");
879
+ if (semanticCommand) {
880
+ semanticCommand.getArgumentCompletions = (prefix) => this.getSemanticArgumentCompletions(prefix);
881
+ }
841
882
  // Convert prompt templates to SlashCommand format for autocomplete
842
883
  const templateCommands = this.session.promptTemplates.map((cmd) => ({
843
884
  name: cmd.name,
@@ -1091,7 +1132,9 @@ export class InteractiveMode {
1091
1132
  }
1092
1133
  // Commands and keys
1093
1134
  const commandsLine = `${pad}${theme.fg("dim", "cmds")} ` +
1094
- ["model", "login", "mcp", "memory", "doctor", "new"].map((c) => theme.fg("accent", `/${c}`)).join(theme.fg("dim", " "));
1135
+ ["model", "login", "mcp", "semantic", "memory", "doctor", "new"]
1136
+ .map((c) => theme.fg("accent", `/${c}`))
1137
+ .join(theme.fg("dim", " "));
1095
1138
  const keysLine = `${pad}${theme.fg("dim", "keys")} ` +
1096
1139
  appKeyHint(kb, "interrupt", "stop") +
1097
1140
  theme.fg("dim", " ") +
@@ -1985,6 +2028,28 @@ export class InteractiveMode {
1985
2028
  const result = await this.showExtensionSelector(`${title}\n${message}`, ["Yes", "No"], opts);
1986
2029
  return result === "Yes";
1987
2030
  }
2031
+ async runWithExtensionLoader(message, task) {
2032
+ const loader = new BorderedLoader(this.ui, theme, message, {
2033
+ cancellable: false,
2034
+ });
2035
+ this.editorContainer.clear();
2036
+ this.editorContainer.addChild(loader);
2037
+ this.ui.setFocus(loader);
2038
+ this.ui.requestRender();
2039
+ const restoreEditor = () => {
2040
+ loader.dispose();
2041
+ this.editorContainer.clear();
2042
+ this.editorContainer.addChild(this.editor);
2043
+ this.ui.setFocus(this.editor);
2044
+ this.ui.requestRender();
2045
+ };
2046
+ try {
2047
+ return await task();
2048
+ }
2049
+ finally {
2050
+ restoreEditor();
2051
+ }
2052
+ }
1988
2053
  /**
1989
2054
  * Show a text input for extensions.
1990
2055
  */
@@ -2384,6 +2449,11 @@ export class InteractiveMode {
2384
2449
  await this.handleMemoryCommand(text);
2385
2450
  return;
2386
2451
  }
2452
+ if (text === "/semantic" || text.startsWith("/semantic ")) {
2453
+ this.editor.setText("");
2454
+ await this.handleSemanticCommand(text);
2455
+ return;
2456
+ }
2387
2457
  if (text === "/settings") {
2388
2458
  this.showSettingsSelector();
2389
2459
  this.editor.setText("");
@@ -5018,7 +5088,8 @@ export class InteractiveMode {
5018
5088
  const providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((p) => p.id === providerId);
5019
5089
  return providerInfo?.name || providerId;
5020
5090
  }
5021
- async handleOpenRouterApiKeyLogin() {
5091
+ async handleOpenRouterApiKeyLogin(options) {
5092
+ const openModelSelector = options?.openModelSelector ?? true;
5022
5093
  const providerName = this.getProviderDisplayName(OPENROUTER_PROVIDER_ID);
5023
5094
  const existingCredential = this.session.modelRegistry.authStorage.get(OPENROUTER_PROVIDER_ID);
5024
5095
  if (existingCredential) {
@@ -5041,7 +5112,9 @@ export class InteractiveMode {
5041
5112
  this.session.modelRegistry.authStorage.set(OPENROUTER_PROVIDER_ID, { type: "api_key", key: apiKey });
5042
5113
  await this.updateAvailableProviderCount();
5043
5114
  this.showStatus(`${providerName} API key saved to ${getAuthPath()}`);
5044
- await this.showModelProviderSelector(OPENROUTER_PROVIDER_ID);
5115
+ if (openModelSelector) {
5116
+ await this.showModelProviderSelector(OPENROUTER_PROVIDER_ID);
5117
+ }
5045
5118
  }
5046
5119
  async showLoginDialog(providerId) {
5047
5120
  const providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((p) => p.id === providerId);
@@ -5499,6 +5572,567 @@ export class InteractiveMode {
5499
5572
  this.showError(error instanceof Error ? error.message : String(error));
5500
5573
  }
5501
5574
  }
5575
+ createSemanticRuntime() {
5576
+ return new SemanticSearchRuntime({
5577
+ cwd: this.sessionManager.getCwd(),
5578
+ agentDir: getAgentDir(),
5579
+ authStorage: this.session.modelRegistry.authStorage,
5580
+ });
5581
+ }
5582
+ parseSemanticScopeOptions(args) {
5583
+ let scope;
5584
+ const rest = [];
5585
+ for (let index = 0; index < args.length; index++) {
5586
+ const token = args[index] ?? "";
5587
+ const normalized = token.toLowerCase();
5588
+ if (normalized === "--scope") {
5589
+ const value = (args[index + 1] ?? "").toLowerCase();
5590
+ if (!value) {
5591
+ return { scope, rest, error: "Usage: /semantic setup --scope <user|project>" };
5592
+ }
5593
+ if (value !== "user" && value !== "project") {
5594
+ return { scope, rest, error: `Invalid semantic scope "${value}". Use user or project.` };
5595
+ }
5596
+ scope = value;
5597
+ index += 1;
5598
+ continue;
5599
+ }
5600
+ rest.push(token);
5601
+ }
5602
+ return { scope, rest };
5603
+ }
5604
+ parseSemanticTopKOptions(args) {
5605
+ let topK;
5606
+ const rest = [];
5607
+ for (let index = 0; index < args.length; index++) {
5608
+ const token = args[index] ?? "";
5609
+ const normalized = token.toLowerCase();
5610
+ if (normalized === "--top-k" || normalized === "--topk") {
5611
+ const raw = (args[index + 1] ?? "").trim();
5612
+ if (!raw) {
5613
+ return { topK, rest, error: "Usage: /semantic query <text> [--top-k 1..20]" };
5614
+ }
5615
+ const parsed = Number.parseInt(raw, 10);
5616
+ if (!Number.isFinite(parsed) || `${parsed}` !== raw || parsed < 1 || parsed > 20) {
5617
+ return { topK, rest, error: "--top-k must be an integer between 1 and 20." };
5618
+ }
5619
+ topK = parsed;
5620
+ index += 1;
5621
+ continue;
5622
+ }
5623
+ rest.push(token);
5624
+ }
5625
+ return { topK, rest };
5626
+ }
5627
+ formatSemanticStatusReport(status) {
5628
+ const lines = [
5629
+ `configured: ${status.configured ? "yes" : "no"}`,
5630
+ `enabled: ${status.enabled ? "yes" : "no"}`,
5631
+ `indexed: ${status.indexed ? "yes" : "no"}`,
5632
+ `stale: ${status.stale ? `yes${status.staleReason ? ` (${status.staleReason})` : ""}` : "no"}`,
5633
+ ];
5634
+ if (status.provider)
5635
+ lines.push(`provider: ${status.provider}`);
5636
+ if (status.model)
5637
+ lines.push(`model: ${status.model}`);
5638
+ lines.push(`files: ${status.files}`);
5639
+ lines.push(`chunks: ${status.chunks}`);
5640
+ if (status.dimension !== undefined)
5641
+ lines.push(`dimension: ${status.dimension}`);
5642
+ if (status.ageSeconds !== undefined)
5643
+ lines.push(`age_seconds: ${status.ageSeconds}`);
5644
+ lines.push(`index_path: ${status.indexPath}`);
5645
+ lines.push(`config_user: ${status.configPathUser}`);
5646
+ lines.push(`config_project: ${status.configPathProject}`);
5647
+ if (!status.configured) {
5648
+ lines.push("hint: run /semantic setup");
5649
+ }
5650
+ return lines.join("\n");
5651
+ }
5652
+ formatSemanticIndexReport(result) {
5653
+ return [
5654
+ `action: ${result.action}`,
5655
+ `processed_files: ${result.processedFiles}`,
5656
+ `reused_files: ${result.reusedFiles}`,
5657
+ `new_files: ${result.newFiles}`,
5658
+ `changed_files: ${result.changedFiles}`,
5659
+ `removed_files: ${result.removedFiles}`,
5660
+ `chunks: ${result.chunks}`,
5661
+ `dimension: ${result.dimension}`,
5662
+ `duration_ms: ${result.durationMs}`,
5663
+ `built_at: ${result.builtAt}`,
5664
+ `index_path: ${result.indexPath}`,
5665
+ ].join("\n");
5666
+ }
5667
+ formatSemanticQueryReport(result) {
5668
+ const lines = [
5669
+ `query: ${result.query}`,
5670
+ `top_k: ${result.topK}`,
5671
+ `auto_refreshed: ${result.autoRefreshed ? "yes" : "no"}`,
5672
+ ];
5673
+ if (result.hits.length === 0) {
5674
+ lines.push("hits: none");
5675
+ return lines.join("\n");
5676
+ }
5677
+ lines.push("hits:");
5678
+ for (let index = 0; index < result.hits.length; index++) {
5679
+ const hit = result.hits[index];
5680
+ lines.push(`${index + 1}. score=${hit.score.toFixed(4)} ${hit.path}:${hit.lineStart}-${hit.lineEnd}`);
5681
+ lines.push(` ${hit.snippet}`);
5682
+ }
5683
+ return lines.join("\n");
5684
+ }
5685
+ getSemanticSetupProviderLabel(type) {
5686
+ if (type === "openrouter")
5687
+ return "openrouter";
5688
+ if (type === "ollama")
5689
+ return "ollama";
5690
+ return "custom_openai";
5691
+ }
5692
+ async ensureOpenRouterSemanticCredentials() {
5693
+ const existing = await this.session.modelRegistry.authStorage.getApiKey(OPENROUTER_PROVIDER_ID);
5694
+ if (existing)
5695
+ return;
5696
+ const shouldLogin = await this.showExtensionConfirm("Semantic setup: OpenRouter key missing", "No OpenRouter API key found. Open login flow now?");
5697
+ if (!shouldLogin) {
5698
+ this.showWarning("OpenRouter key is missing. semantic index/query will fail until credentials are added.");
5699
+ return;
5700
+ }
5701
+ await this.handleOpenRouterApiKeyLogin({ openModelSelector: false });
5702
+ const afterLogin = await this.session.modelRegistry.authStorage.getApiKey(OPENROUTER_PROVIDER_ID);
5703
+ if (!afterLogin) {
5704
+ this.showWarning("OpenRouter key is still missing. You can run /login later.");
5705
+ }
5706
+ }
5707
+ async selectSemanticModelFromCatalog(title, catalogModels, currentModel, options) {
5708
+ const normalizedCurrent = currentModel.trim();
5709
+ const uniqueModels = [];
5710
+ const seen = new Set();
5711
+ for (const model of catalogModels) {
5712
+ const normalized = model.trim();
5713
+ if (!normalized || seen.has(normalized))
5714
+ continue;
5715
+ seen.add(normalized);
5716
+ uniqueModels.push(normalized);
5717
+ }
5718
+ const optionToModel = new Map();
5719
+ const selectorOptions = [];
5720
+ const addOption = (label, modelId) => {
5721
+ let uniqueLabel = label;
5722
+ let suffix = 2;
5723
+ while (optionToModel.has(uniqueLabel)) {
5724
+ uniqueLabel = `${label} (${suffix})`;
5725
+ suffix += 1;
5726
+ }
5727
+ optionToModel.set(uniqueLabel, modelId);
5728
+ selectorOptions.push(uniqueLabel);
5729
+ };
5730
+ if (normalizedCurrent) {
5731
+ addOption(`Current: ${normalizedCurrent}`, normalizedCurrent);
5732
+ }
5733
+ for (const modelId of uniqueModels) {
5734
+ const marker = options?.highlightLikelyEmbedding && isLikelyEmbeddingModelId(modelId) ? " [embedding]" : "";
5735
+ addOption(`${modelId}${marker}`, modelId);
5736
+ }
5737
+ const manualOption = "Enter model manually";
5738
+ selectorOptions.push(manualOption);
5739
+ const selected = await this.showExtensionSelector(title, selectorOptions);
5740
+ if (!selected)
5741
+ return undefined;
5742
+ if (selected === manualOption) {
5743
+ const modelInput = await this.showExtensionInput(`${title}: model`, normalizedCurrent || "model-id");
5744
+ if (modelInput === undefined)
5745
+ return undefined;
5746
+ const model = modelInput.trim();
5747
+ if (!model) {
5748
+ this.showWarning("Model cannot be empty.");
5749
+ return undefined;
5750
+ }
5751
+ return model;
5752
+ }
5753
+ return optionToModel.get(selected);
5754
+ }
5755
+ async runSemanticSetupWizard(initialScope) {
5756
+ const cwd = this.sessionManager.getCwd();
5757
+ const agentDir = getAgentDir();
5758
+ const merged = loadMergedSemanticConfig(cwd, agentDir);
5759
+ const config = merged.config ?? getDefaultSemanticSearchConfig();
5760
+ let scope = initialScope;
5761
+ if (!scope) {
5762
+ const pickedScope = await this.showExtensionSelector("/semantic setup: scope", [
5763
+ "user (Recommended)",
5764
+ "project",
5765
+ ]);
5766
+ if (!pickedScope) {
5767
+ this.showStatus("Semantic setup cancelled");
5768
+ return;
5769
+ }
5770
+ scope = pickedScope.startsWith("project") ? "project" : "user";
5771
+ }
5772
+ const providerOption = await this.showExtensionSelector("/semantic setup: provider", [
5773
+ "openrouter (Recommended)",
5774
+ "ollama",
5775
+ "custom_openai",
5776
+ ]);
5777
+ if (!providerOption) {
5778
+ this.showStatus("Semantic setup cancelled");
5779
+ return;
5780
+ }
5781
+ const providerType = providerOption.startsWith("ollama")
5782
+ ? "ollama"
5783
+ : providerOption.startsWith("custom_openai")
5784
+ ? "custom_openai"
5785
+ : "openrouter";
5786
+ const providerDefaults = {
5787
+ openrouter: "openai/text-embedding-3-small",
5788
+ ollama: "nomic-embed-text",
5789
+ custom_openai: "text-embedding-3-small",
5790
+ };
5791
+ const currentModel = (config.provider.type === providerType ? config.provider.model : providerDefaults[providerType]).trim();
5792
+ let model = currentModel;
5793
+ const nextProvider = {
5794
+ ...config.provider,
5795
+ type: providerType,
5796
+ model: model || providerDefaults[providerType],
5797
+ };
5798
+ if (providerType === "ollama") {
5799
+ const defaultBase = (config.provider.type === "ollama" ? config.provider.baseUrl : undefined) ?? "http://127.0.0.1:11434";
5800
+ const baseUrlInput = await this.showExtensionInput("/semantic setup: ollama base URL", defaultBase);
5801
+ if (baseUrlInput === undefined) {
5802
+ this.showStatus("Semantic setup cancelled");
5803
+ return;
5804
+ }
5805
+ const baseUrl = baseUrlInput.trim();
5806
+ nextProvider.baseUrl = baseUrl || undefined;
5807
+ nextProvider.apiKeyEnv = undefined;
5808
+ let ollamaModels = [];
5809
+ try {
5810
+ ollamaModels = await listOllamaLocalModels({
5811
+ baseUrl: nextProvider.baseUrl,
5812
+ headers: config.provider.type === "ollama" ? config.provider.headers : undefined,
5813
+ timeoutMs: nextProvider.timeoutMs,
5814
+ });
5815
+ if (ollamaModels.length === 0) {
5816
+ this.showWarning("No local Ollama models found at /api/tags. You can enter model manually.");
5817
+ }
5818
+ }
5819
+ catch (error) {
5820
+ const errorMsg = error instanceof Error ? error.message : String(error);
5821
+ this.showWarning(`Failed to load Ollama models automatically: ${errorMsg}`);
5822
+ }
5823
+ const selectedModel = await this.selectSemanticModelFromCatalog("/semantic setup: ollama model", ollamaModels, currentModel || providerDefaults.ollama, { highlightLikelyEmbedding: true });
5824
+ if (!selectedModel) {
5825
+ this.showStatus("Semantic setup cancelled");
5826
+ return;
5827
+ }
5828
+ model = selectedModel;
5829
+ }
5830
+ else if (providerType === "custom_openai") {
5831
+ const defaultBase = (config.provider.type === "custom_openai" ? config.provider.baseUrl : undefined) ?? "http://127.0.0.1:8000/v1";
5832
+ const baseUrlInput = await this.showExtensionInput("/semantic setup: custom base URL", defaultBase);
5833
+ if (baseUrlInput === undefined) {
5834
+ this.showStatus("Semantic setup cancelled");
5835
+ return;
5836
+ }
5837
+ const baseUrl = baseUrlInput.trim();
5838
+ if (!baseUrl) {
5839
+ this.showWarning("Custom provider base URL cannot be empty.");
5840
+ return;
5841
+ }
5842
+ nextProvider.baseUrl = baseUrl;
5843
+ const defaultApiKeyEnv = (config.provider.type === "custom_openai" ? config.provider.apiKeyEnv : undefined) ?? "OPENAI_API_KEY";
5844
+ const apiKeyEnvInput = await this.showExtensionInput("/semantic setup: custom API key env (optional)", defaultApiKeyEnv);
5845
+ if (apiKeyEnvInput === undefined) {
5846
+ this.showStatus("Semantic setup cancelled");
5847
+ return;
5848
+ }
5849
+ nextProvider.apiKeyEnv = apiKeyEnvInput.trim() || undefined;
5850
+ const modelInput = await this.showExtensionInput("/semantic setup: custom model", currentModel);
5851
+ if (modelInput === undefined) {
5852
+ this.showStatus("Semantic setup cancelled");
5853
+ return;
5854
+ }
5855
+ const normalizedModel = modelInput.trim();
5856
+ if (!normalizedModel) {
5857
+ this.showWarning("Model cannot be empty.");
5858
+ return;
5859
+ }
5860
+ model = normalizedModel;
5861
+ }
5862
+ else {
5863
+ nextProvider.baseUrl = undefined;
5864
+ nextProvider.apiKeyEnv = undefined;
5865
+ let openRouterModels = [];
5866
+ try {
5867
+ openRouterModels = await listOpenRouterEmbeddingModels({
5868
+ timeoutMs: nextProvider.timeoutMs,
5869
+ authStorage: this.session.modelRegistry.authStorage,
5870
+ });
5871
+ if (openRouterModels.length === 0) {
5872
+ this.showWarning("OpenRouter embeddings catalog is empty. You can enter model manually.");
5873
+ }
5874
+ }
5875
+ catch (error) {
5876
+ const errorMsg = error instanceof Error ? error.message : String(error);
5877
+ this.showWarning(`Failed to load OpenRouter embedding models automatically: ${errorMsg}`);
5878
+ }
5879
+ const selectedModel = await this.selectSemanticModelFromCatalog("/semantic setup: openrouter model", openRouterModels, currentModel || providerDefaults.openrouter);
5880
+ if (!selectedModel) {
5881
+ this.showStatus("Semantic setup cancelled");
5882
+ return;
5883
+ }
5884
+ model = selectedModel;
5885
+ }
5886
+ nextProvider.model = model;
5887
+ while (true) {
5888
+ const headersInput = await this.showExtensionInput("/semantic setup: headers (optional KEY=VALUE,CSV; press Enter to skip)", "");
5889
+ if (headersInput === undefined) {
5890
+ this.showStatus("Semantic setup cancelled");
5891
+ return;
5892
+ }
5893
+ const parsedHeaders = this.parseMcpKeyValueMapInput(headersInput);
5894
+ if (parsedHeaders.error) {
5895
+ this.showWarning(parsedHeaders.error);
5896
+ continue;
5897
+ }
5898
+ nextProvider.headers = parsedHeaders.value;
5899
+ break;
5900
+ }
5901
+ const nextConfig = {
5902
+ ...config,
5903
+ enabled: true,
5904
+ provider: nextProvider,
5905
+ };
5906
+ const savedPath = upsertScopedSemanticSearchConfig(scope, nextConfig, cwd, agentDir);
5907
+ if (providerType === "openrouter") {
5908
+ await this.ensureOpenRouterSemanticCredentials();
5909
+ }
5910
+ this.showStatus(`Semantic setup saved (${scope})`);
5911
+ this.showCommandTextBlock("Semantic Setup", [
5912
+ `scope: ${scope}`,
5913
+ `provider: ${this.getSemanticSetupProviderLabel(providerType)}`,
5914
+ `model: ${nextProvider.model}`,
5915
+ `config: ${savedPath}`,
5916
+ `index_dir: ${getSemanticIndexDir(cwd, agentDir)}`,
5917
+ ].join("\n"));
5918
+ }
5919
+ async runSemanticInteractiveMenu() {
5920
+ while (true) {
5921
+ let status;
5922
+ try {
5923
+ status = await this.createSemanticRuntime().status();
5924
+ }
5925
+ catch (error) {
5926
+ this.reportSemanticError(error, "status");
5927
+ }
5928
+ const summary = status
5929
+ ? `configured=${status.configured ? "yes" : "no"} indexed=${status.indexed ? "yes" : "no"} stale=${status.stale ? "yes" : "no"}`
5930
+ : "status unavailable";
5931
+ const selected = await this.showExtensionSelector(`/semantic manager\n${summary}`, [
5932
+ "Configure provider/model",
5933
+ "Show status",
5934
+ "Index now",
5935
+ "Rebuild index",
5936
+ "Query index",
5937
+ "Show config/index paths",
5938
+ "Close",
5939
+ ]);
5940
+ if (!selected || selected === "Close") {
5941
+ return;
5942
+ }
5943
+ if (selected === "Configure provider/model") {
5944
+ await this.runSemanticSetupWizard();
5945
+ continue;
5946
+ }
5947
+ if (selected === "Show status") {
5948
+ try {
5949
+ const result = await this.runWithExtensionLoader("Checking semantic index status...", async () => this.createSemanticRuntime().status());
5950
+ this.showCommandTextBlock("Semantic Status", this.formatSemanticStatusReport(result));
5951
+ }
5952
+ catch (error) {
5953
+ this.reportSemanticError(error, "status");
5954
+ }
5955
+ continue;
5956
+ }
5957
+ if (selected === "Index now") {
5958
+ try {
5959
+ const result = await this.runWithExtensionLoader("Indexing semantic embeddings...", async () => this.createSemanticRuntime().index());
5960
+ this.showStatus(`Semantic index updated (${result.processedFiles} files).`);
5961
+ this.showCommandTextBlock("Semantic Index", this.formatSemanticIndexReport(result));
5962
+ }
5963
+ catch (error) {
5964
+ this.reportSemanticError(error, "index");
5965
+ }
5966
+ continue;
5967
+ }
5968
+ if (selected === "Rebuild index") {
5969
+ try {
5970
+ const result = await this.runWithExtensionLoader("Rebuilding semantic index...", async () => this.createSemanticRuntime().rebuild());
5971
+ this.showStatus(`Semantic index rebuilt (${result.processedFiles} files).`);
5972
+ this.showCommandTextBlock("Semantic Rebuild", this.formatSemanticIndexReport(result));
5973
+ }
5974
+ catch (error) {
5975
+ this.reportSemanticError(error, "rebuild");
5976
+ }
5977
+ continue;
5978
+ }
5979
+ if (selected === "Query index") {
5980
+ const queryInput = await this.showExtensionInput("/semantic query", "where auth token is validated");
5981
+ if (queryInput === undefined)
5982
+ continue;
5983
+ const query = queryInput.trim();
5984
+ if (!query) {
5985
+ this.showWarning("Semantic query cannot be empty.");
5986
+ continue;
5987
+ }
5988
+ const topKInput = await this.showExtensionInput("/semantic query: top-k (optional, 1..20)", "8");
5989
+ if (topKInput === undefined)
5990
+ continue;
5991
+ const topKRaw = topKInput.trim();
5992
+ let topK = undefined;
5993
+ if (topKRaw) {
5994
+ const parsed = Number.parseInt(topKRaw, 10);
5995
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 20) {
5996
+ this.showWarning("top-k must be an integer between 1 and 20.");
5997
+ continue;
5998
+ }
5999
+ topK = parsed;
6000
+ }
6001
+ try {
6002
+ const result = await this.runWithExtensionLoader("Querying semantic index...", async () => this.createSemanticRuntime().query(query, topK));
6003
+ this.showCommandTextBlock("Semantic Query", this.formatSemanticQueryReport(result));
6004
+ }
6005
+ catch (error) {
6006
+ this.reportSemanticError(error, "query");
6007
+ }
6008
+ continue;
6009
+ }
6010
+ if (selected === "Show config/index paths") {
6011
+ const runtime = this.createSemanticRuntime();
6012
+ const cwd = this.sessionManager.getCwd();
6013
+ const agentDir = getAgentDir();
6014
+ try {
6015
+ const semanticStatus = await this.runWithExtensionLoader("Loading semantic paths...", async () => runtime.status());
6016
+ this.showCommandTextBlock("Semantic Paths", [
6017
+ `user_config: ${getSemanticConfigPath("user", cwd, agentDir)}`,
6018
+ `project_config: ${getSemanticConfigPath("project", cwd, agentDir)}`,
6019
+ `index_dir: ${semanticStatus.indexPath}`,
6020
+ ].join("\n"));
6021
+ }
6022
+ catch (error) {
6023
+ this.reportSemanticError(error, "status");
6024
+ }
6025
+ continue;
6026
+ }
6027
+ }
6028
+ }
6029
+ reportSemanticError(error, context) {
6030
+ if (error instanceof SemanticConfigMissingError) {
6031
+ this.showWarning("Semantic search is not configured. Run /semantic setup.");
6032
+ this.showCommandTextBlock("Semantic Config", [
6033
+ `user_config: ${error.userConfigPath}`,
6034
+ `project_config: ${error.projectConfigPath}`,
6035
+ "next: /semantic setup",
6036
+ ].join("\n"));
6037
+ return;
6038
+ }
6039
+ if (error instanceof SemanticRebuildRequiredError) {
6040
+ this.showWarning(error.message);
6041
+ this.showWarning("Run /semantic rebuild.");
6042
+ return;
6043
+ }
6044
+ const message = error instanceof Error ? error.message : String(error);
6045
+ this.showError(`Semantic ${context} failed: ${message}`);
6046
+ }
6047
+ async handleSemanticCommand(text) {
6048
+ const args = this.parseSlashArgs(text).slice(1);
6049
+ if (args.length === 0 || (args[0]?.toLowerCase() ?? "") === "ui") {
6050
+ await this.runSemanticInteractiveMenu();
6051
+ return;
6052
+ }
6053
+ const subcommand = (args[0] ?? "").toLowerCase();
6054
+ const rest = args.slice(1);
6055
+ if (subcommand === "help" || subcommand === "-h" || subcommand === "--help") {
6056
+ this.showCommandTextBlock("Semantic Help", [
6057
+ "Usage:",
6058
+ " /semantic",
6059
+ " /semantic ui",
6060
+ " /semantic setup [--scope user|project]",
6061
+ " /semantic status",
6062
+ " /semantic index",
6063
+ " /semantic rebuild",
6064
+ " /semantic query <text> [--top-k N]",
6065
+ " /semantic help",
6066
+ ].join("\n"));
6067
+ return;
6068
+ }
6069
+ if (subcommand === "setup") {
6070
+ const parsedScope = this.parseSemanticScopeOptions(rest);
6071
+ if (parsedScope.error) {
6072
+ this.showWarning(parsedScope.error);
6073
+ return;
6074
+ }
6075
+ if (parsedScope.rest.length > 0) {
6076
+ this.showWarning(`Unexpected arguments for /semantic setup: ${parsedScope.rest.join(" ")}`);
6077
+ return;
6078
+ }
6079
+ await this.runSemanticSetupWizard(parsedScope.scope);
6080
+ return;
6081
+ }
6082
+ if (subcommand === "status") {
6083
+ try {
6084
+ const result = await this.runWithExtensionLoader("Checking semantic index status...", async () => this.createSemanticRuntime().status());
6085
+ this.showCommandTextBlock("Semantic Status", this.formatSemanticStatusReport(result));
6086
+ }
6087
+ catch (error) {
6088
+ this.reportSemanticError(error, "status");
6089
+ }
6090
+ return;
6091
+ }
6092
+ if (subcommand === "index") {
6093
+ try {
6094
+ const result = await this.runWithExtensionLoader("Indexing semantic embeddings...", async () => this.createSemanticRuntime().index());
6095
+ this.showStatus(`Semantic index updated (${result.processedFiles} files).`);
6096
+ this.showCommandTextBlock("Semantic Index", this.formatSemanticIndexReport(result));
6097
+ }
6098
+ catch (error) {
6099
+ this.reportSemanticError(error, "index");
6100
+ }
6101
+ return;
6102
+ }
6103
+ if (subcommand === "rebuild") {
6104
+ try {
6105
+ const result = await this.runWithExtensionLoader("Rebuilding semantic index...", async () => this.createSemanticRuntime().rebuild());
6106
+ this.showStatus(`Semantic index rebuilt (${result.processedFiles} files).`);
6107
+ this.showCommandTextBlock("Semantic Rebuild", this.formatSemanticIndexReport(result));
6108
+ }
6109
+ catch (error) {
6110
+ this.reportSemanticError(error, "rebuild");
6111
+ }
6112
+ return;
6113
+ }
6114
+ if (subcommand === "query") {
6115
+ const parsed = this.parseSemanticTopKOptions(rest);
6116
+ if (parsed.error) {
6117
+ this.showWarning(parsed.error);
6118
+ return;
6119
+ }
6120
+ const query = parsed.rest.join(" ").trim();
6121
+ if (!query) {
6122
+ this.showWarning("Usage: /semantic query <text> [--top-k N]");
6123
+ return;
6124
+ }
6125
+ try {
6126
+ const result = await this.runWithExtensionLoader("Querying semantic index...", async () => this.createSemanticRuntime().query(query, parsed.topK));
6127
+ this.showCommandTextBlock("Semantic Query", this.formatSemanticQueryReport(result));
6128
+ }
6129
+ catch (error) {
6130
+ this.reportSemanticError(error, "query");
6131
+ }
6132
+ return;
6133
+ }
6134
+ this.showWarning(`Unknown /semantic subcommand "${subcommand}". Use /semantic help.`);
6135
+ }
5502
6136
  parseCheckpointNameFromLabel(label) {
5503
6137
  if (!label)
5504
6138
  return undefined;
@@ -5680,6 +6314,7 @@ export class InteractiveMode {
5680
6314
  const hasModelIssues = checks.some((check) => (check.label === "Active model" || check.label === "Active model auth" || check.label === "Available models") &&
5681
6315
  check.level === "fail");
5682
6316
  const hasMcpIssues = checks.some((check) => check.label === "MCP servers" && check.level !== "ok");
6317
+ const hasSemanticIssues = checks.some((check) => check.label === "Semantic index" && check.level !== "ok");
5683
6318
  const hasResourceIssues = checks.some((check) => check.label === "Resources" && check.level !== "ok");
5684
6319
  while (true) {
5685
6320
  const options = [];
@@ -5693,6 +6328,9 @@ export class InteractiveMode {
5693
6328
  }
5694
6329
  options.push("Refresh MCP runtime");
5695
6330
  }
6331
+ if (hasSemanticIssues) {
6332
+ options.push("Open semantic manager");
6333
+ }
5696
6334
  if (this.permissionMode === "yolo") {
5697
6335
  options.push("Set permissions mode to ask");
5698
6336
  }
@@ -5723,6 +6361,10 @@ export class InteractiveMode {
5723
6361
  this.showStatus("MCP servers refreshed");
5724
6362
  continue;
5725
6363
  }
6364
+ if (selected === "Open semantic manager") {
6365
+ await this.handleSemanticCommand("/semantic");
6366
+ return;
6367
+ }
5726
6368
  if (selected === "Set permissions mode to ask") {
5727
6369
  this.permissionMode = "ask";
5728
6370
  this.settingsManager.setPermissionMode("ask");
@@ -5734,7 +6376,15 @@ export class InteractiveMode {
5734
6376
  return;
5735
6377
  }
5736
6378
  if (selected === "Show auth/models paths") {
5737
- this.showCommandTextBlock("Runtime Paths", [`auth.json: ${getAuthPath()}`, `models.json: ${getModelsPath()}`].join("\n"));
6379
+ const cwd = this.sessionManager.getCwd();
6380
+ const agentDir = getAgentDir();
6381
+ this.showCommandTextBlock("Runtime Paths", [
6382
+ `auth.json: ${getAuthPath()}`,
6383
+ `models.json: ${getModelsPath()}`,
6384
+ `semantic(user): ${getSemanticConfigPath("user", cwd, agentDir)}`,
6385
+ `semantic(project): ${getSemanticConfigPath("project", cwd, agentDir)}`,
6386
+ `semantic(index): ${getSemanticIndexDir(cwd, agentDir)}`,
6387
+ ].join("\n"));
5738
6388
  continue;
5739
6389
  }
5740
6390
  }
@@ -5761,6 +6411,14 @@ export class InteractiveMode {
5761
6411
  const mcpDisabled = mcpStatuses.filter((status) => !status.enabled).length;
5762
6412
  const cliToolStatuses = resolveDoctorCliToolStatuses();
5763
6413
  const missingCliTools = cliToolStatuses.filter((status) => !status.available).map((status) => status.tool);
6414
+ let semanticStatus;
6415
+ let semanticStatusError;
6416
+ try {
6417
+ semanticStatus = await this.createSemanticRuntime().status();
6418
+ }
6419
+ catch (error) {
6420
+ semanticStatusError = error instanceof Error ? error.message : String(error);
6421
+ }
5764
6422
  const checks = [];
5765
6423
  const addCheck = (level, label, detail, fix) => checks.push({ level, label, detail, fix });
5766
6424
  if (!model) {
@@ -5802,6 +6460,31 @@ export class InteractiveMode {
5802
6460
  else {
5803
6461
  addCheck("ok", "MCP servers", `${mcpConnected} connected, ${mcpDisabled} disabled`);
5804
6462
  }
6463
+ if (semanticStatusError) {
6464
+ addCheck("fail", "Semantic index", semanticStatusError, "Run /semantic setup and retry /semantic status.");
6465
+ }
6466
+ else if (!semanticStatus) {
6467
+ addCheck("warn", "Semantic index", "Status unavailable", "Run /semantic setup.");
6468
+ }
6469
+ else if (!semanticStatus.configured) {
6470
+ addCheck("warn", "Semantic index", "Not configured", `Run /semantic setup (user: ${semanticStatus.configPathUser} or project: ${semanticStatus.configPathProject}).`);
6471
+ }
6472
+ else if (!semanticStatus.enabled) {
6473
+ addCheck("warn", "Semantic index", "Configured but disabled", "Enable semanticSearch.enabled or rerun /semantic setup.");
6474
+ }
6475
+ else if (!semanticStatus.indexed) {
6476
+ addCheck("warn", "Semantic index", "Configured but index is missing", "Run /semantic index.");
6477
+ }
6478
+ else if (semanticStatus.stale) {
6479
+ const requiresRebuild = semanticStatus.staleReason === "provider_changed" ||
6480
+ semanticStatus.staleReason === "chunking_changed" ||
6481
+ semanticStatus.staleReason === "index_filters_changed" ||
6482
+ semanticStatus.staleReason === "dimension_mismatch";
6483
+ addCheck("warn", "Semantic index", `Indexed but stale${semanticStatus.staleReason ? ` (${semanticStatus.staleReason})` : ""}`, requiresRebuild ? "Run /semantic rebuild." : "Run /semantic index.");
6484
+ }
6485
+ else {
6486
+ addCheck("ok", "Semantic index", `${semanticStatus.provider}/${semanticStatus.model} · files=${semanticStatus.files} chunks=${semanticStatus.chunks}`);
6487
+ }
5805
6488
  if (missingCliTools.length > 0) {
5806
6489
  addCheck("warn", "CLI toolchain", `${cliToolStatuses.length - missingCliTools.length}/${cliToolStatuses.length} available (missing: ${missingCliTools.join(", ")})`, `Install missing CLI tools: ${missingCliTools.join(", ")}.`);
5807
6490
  }