fss-link 1.4.0 โ†’ 1.4.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/README.md CHANGED
@@ -12,17 +12,17 @@
12
12
 
13
13
  </div>
14
14
 
15
- **FSS Link** is a portable, scriptable agent that links tools, models, and knowledge into multi-instance workflows. Install anywhere with Node.js, launch 1-8 agents at once, feed them templated tasks, and watch them go.
15
+ **FSS Link** is a portable, scriptable agent that links tools, models, and knowledge into single-instance workflows. Install anywhere with Node.js, launch one agent at a time, feed it tasks, and watch it go.
16
16
 
17
17
  - **Lightweight distribution** (~15 MB bundle, requires Node.js 20+)
18
- - **Spawn many agents** with structured prompts
19
- - **Portable Mini-RAG** indices that travel with your projects
18
+ - **Single-agent workflows** with structured prompts
19
+ - **Portable Mini-RAG** indices that travel with your projects (planned feature)
20
20
  - **Local-first** with LM Studio/Ollama priority
21
21
  - **Professional tools** built in TypeScript for seamless integration
22
22
 
23
23
  ## ๐ŸŽฏ Why "Link"?
24
24
 
25
- Link connects thingsโ€”tools, models, tasks, and knowledge. **FSS Link** is a lightweight distribution that runs wherever Node.js is installed. Start an agent, or spawn eight in parallel with templated prompts. Each project carries its own Mini-RAG index, so knowledge travels with the work.
25
+ Link connects thingsโ€”tools, models, tasks, and knowledge. **FSS Link** is a lightweight distribution that runs wherever Node.js is installed. Start an agent, or spawn multiple agents with templated prompts (planned feature). Each project carries its own Mini-RAG index, so knowledge travels with the work.
26
26
 
27
27
  ## ๐Ÿš€ Quick Start
28
28
 
@@ -33,10 +33,7 @@ fss-link
33
33
  # Single-shot task
34
34
  fss-link run "Audit the repo for security keys and open PR fixes."
35
35
 
36
- # Launch 6 parallel agents with templated plan
37
- fss-link spawn --count 6 --template ./templates/regression.yml
38
-
39
- # Portable RAG - indexes travel with projects
36
+ # Portable RAG - indexes travel with projects (planned feature)
40
37
  cd project && fss-link rag index . && fss-link rag query "error handling"
41
38
  ```
42
39
 
@@ -98,10 +95,17 @@ FSS Link now features an intelligent **Welcome Back dialog** that appears when y
98
95
 
99
96
  - **๐Ÿ” Project Status Dashboard** - Git status, session history, momentum analysis
100
97
  - **๐Ÿ“Š Smart Context Generation** - Priority-based information filtering (7 levels)
101
- - **โšก Multi-Agent Awareness** - Universal session tracking across all agents
98
+ - **โšก Multi-Agent Awareness** - Universal session tracking across all agents (planned feature)
102
99
  - **๐ŸŽฏ Seamless Resumption** - Intelligent context population for continuing work
103
100
  - **๐Ÿš€ Production Quality** - Comprehensive error handling, performance optimization
104
101
 
102
+ ## ๐Ÿ“ External Directory Access
103
+
104
+ For accessing external directories like `/tmp`, user directories, or custom paths, see:
105
+ [External Directory Access Guide](./fss-docs/EXTERNAL-DIRECTORY-ACCESS.md)
106
+
107
+ This documentation explains how to configure FSS Link to securely access external directories through its folder trust system.
108
+
105
109
  ### ๐Ÿ”„ **Node.js Version Requirements**
106
110
 
107
111
  FSS Link requires **Node.js 20+**. If you have an older version, upgrade first:
@@ -153,7 +157,7 @@ Built-in semantic search that travels with your projects. No Python dependencies
153
157
  - **Deep Semantic Search**: Find code by describing what it does, not just what it's called
154
158
  - **Cross-Reference Analysis**: Discover how different parts of the system interact
155
159
  - **Context-Aware Suggestions**: Get recommendations based on the entire project context
156
- - **Portable Indices**: Knowledge travels with your projects
160
+ - **Portable Indices**: Knowledge travels with your projects (planned feature)
157
161
 
158
162
  ### ๐Ÿ“‹ **Enhanced Task Management**
159
163
  Built on FSS Link's native TodoWrite tool with professional enhancements for complex development workflows.
@@ -161,7 +165,7 @@ Built on FSS Link's native TodoWrite tool with professional enhancements for com
161
165
  - **Persistent Task Lists**: Never lose track of what needs to be done
162
166
  - **Visual Progress Indicators**: Instantly see what's complete, in-progress, or pending
163
167
  - **Intelligent Task Breakdown**: Automatically decompose complex features into actionable steps
164
- - **Multi-Agent Coordination**: Tasks shared across multiple agent instances
168
+ - **Multi-Agent Coordination**: Tasks shared across multiple agent instances (planned feature)
165
169
 
166
170
  ### ๐Ÿ” **Professional Web Research**
167
171
  TypeScript-native web research tools eliminate context switching between development and external resources.
@@ -210,7 +214,7 @@ fss-link
210
214
  # Launch with specific model
211
215
  fss-link --model qwen/qwen3-4b-2507
212
216
 
213
- # Launch with RAG index
217
+ # Launch with RAG index (planned feature)
214
218
  fss-link --rag ./project-index
215
219
  ```
216
220
 
@@ -222,34 +226,34 @@ fss-link run "Analyse this codebase structure"
222
226
  # With specific model
223
227
  fss-link run "Review security patterns" --model qwen/qwen3-30b-a3b-2507
224
228
 
225
- # With RAG context
229
+ # With RAG context (planned feature)
226
230
  fss-link run "Find authentication bugs" --rag .
227
231
  ```
228
232
 
229
- ### **Multi-Agent Workflows**
233
+ ### **Multi-Agent Workflows** (Planned Feature)
230
234
  ```bash
231
- # Launch multiple agents with template
235
+ # Launch multiple agents with template (planned feature)
232
236
  fss-link spawn --count 8 --template ./templates/build-test.yml
233
237
 
234
- # Parallel code review
238
+ # Parallel code review (planned feature)
235
239
  fss-link spawn --count 4 --template ./templates/code-review.yml --files src/
236
240
 
237
- # Load-balanced task processing
241
+ # Load-balanced task processing (planned feature)
238
242
  fss-link spawn --count 6 --template ./templates/regression-test.yml
239
243
  ```
240
244
 
241
- ### **RAG Operations**
245
+ ### **RAG Operations** (Planned Feature)
242
246
  ```bash
243
- # Index current directory
247
+ # Index current directory (planned feature)
244
248
  fss-link rag index .
245
249
 
246
- # Index with specific extensions
250
+ # Index with specific extensions (planned feature)
247
251
  fss-link rag index . --extensions py,js,md
248
252
 
249
- # Query with filters
253
+ # Query with filters (planned feature)
250
254
  fss-link rag query "authentication logic" --topk 5
251
255
 
252
- # Cross-reference search
256
+ # Cross-reference search (planned feature)
253
257
  fss-link rag query "error handling patterns" --files py,js --recent
254
258
  ```
255
259
 
@@ -338,8 +342,8 @@ FSS Link builds on [QwenLM/fss-link](https://github.com/QwenLM/fss-link), which
338
342
 
339
343
  <div align="center">
340
344
 
341
- **FSS Link** - *Portable, scriptable, multi-instance AI agent*
345
+ **FSS Link** - *Portable, scriptable, single-instance AI agent*
342
346
 
343
347
  [Documentation](./FSS-LINK-IMPLEMENTATION-GUIDE.md) โ€ข [Installation Guide](./INSTALL.md) โ€ข [Examples](./examples/)
344
348
 
345
- </div>
349
+ </div>
@@ -22379,7 +22379,7 @@ async function createContentGeneratorConfig(config, authType) {
22379
22379
  async function createContentGenerator(config, gcConfig, sessionId2) {
22380
22380
  if (DEBUG_CONTENT)
22381
22381
  console.log(`\u{1F41B} DEBUG createContentGenerator: authType=${config.authType}, apiKey=${config.apiKey}, baseUrl=${config.baseUrl}`);
22382
- const version = "1.4.0";
22382
+ const version = "1.4.1";
22383
22383
  const userAgent = `FSS-Link/${version} (${process.platform}; ${process.arch})`;
22384
22384
  const baseHeaders = {
22385
22385
  "User-Agent": userAgent
@@ -90220,7 +90220,7 @@ import React31 from "react";
90220
90220
  import { render as render2 } from "ink";
90221
90221
 
90222
90222
  // packages/cli/src/ui/App.tsx
90223
- import { useCallback as useCallback29, useEffect as useEffect43, useMemo as useMemo9, useState as useState48, useRef as useRef12 } from "react";
90223
+ import { useCallback as useCallback29, useEffect as useEffect44, useMemo as useMemo9, useState as useState48, useRef as useRef12 } from "react";
90224
90224
  import {
90225
90225
  Box as Box59,
90226
90226
  measureElement,
@@ -96074,7 +96074,7 @@ async function getPackageJson() {
96074
96074
  // packages/cli/src/utils/version.ts
96075
96075
  async function getCliVersion() {
96076
96076
  const pkgJson = await getPackageJson();
96077
- return "1.4.0";
96077
+ return "1.4.1";
96078
96078
  }
96079
96079
 
96080
96080
  // packages/cli/src/ui/commands/aboutCommand.ts
@@ -96126,7 +96126,7 @@ import open4 from "open";
96126
96126
  import process11 from "node:process";
96127
96127
 
96128
96128
  // packages/cli/src/generated/git-commit.ts
96129
- var GIT_COMMIT_INFO = "92ea8a8d";
96129
+ var GIT_COMMIT_INFO = "b055e021";
96130
96130
 
96131
96131
  // packages/cli/src/ui/commands/bugCommand.ts
96132
96132
  init_dist2();
@@ -120419,8 +120419,60 @@ function LMStudioModelPrompt({
120419
120419
 
120420
120420
  // packages/cli/src/ui/components/AddCustomEndpointDialog.tsx
120421
120421
  init_dist2();
120422
- import { useState as useState33 } from "react";
120422
+ import { useState as useState33, useEffect as useEffect32 } from "react";
120423
120423
  import { Box as Box19, Text as Text26, useInput as useInput4 } from "ink";
120424
+
120425
+ // packages/cli/src/utils/modelFetcher.ts
120426
+ async function fetchModelsFromCustomEndpoint(baseUrl, providerType, apiKey) {
120427
+ const normalizedBase = baseUrl.replace(/\/+$/, "");
120428
+ const url2 = `${normalizedBase}/models`;
120429
+ const headers = {
120430
+ "Accept": "application/json"
120431
+ };
120432
+ if (apiKey && apiKey.trim() !== "") {
120433
+ headers["Authorization"] = `Bearer ${apiKey}`;
120434
+ }
120435
+ try {
120436
+ const controller = new AbortController();
120437
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
120438
+ const response = await fetch(url2, {
120439
+ headers,
120440
+ signal: controller.signal
120441
+ });
120442
+ clearTimeout(timeoutId);
120443
+ if (!response.ok) {
120444
+ if (response.status === 401 || response.status === 403) {
120445
+ throw new Error("Authentication failed - check API key");
120446
+ }
120447
+ if (response.status === 404) {
120448
+ throw new Error("Endpoint not found - verify URL includes /v1 suffix");
120449
+ }
120450
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
120451
+ }
120452
+ const json3 = await response.json();
120453
+ if (!json3.data || !Array.isArray(json3.data)) {
120454
+ throw new Error("Invalid response format - expected OpenAI-compatible /models endpoint");
120455
+ }
120456
+ if (json3.data.length === 0) {
120457
+ throw new Error("No models available on this endpoint");
120458
+ }
120459
+ return json3.data.map((m) => ({
120460
+ id: m.id,
120461
+ ownedBy: m.owned_by || "unknown",
120462
+ object: m.object
120463
+ }));
120464
+ } catch (error) {
120465
+ if (error instanceof Error) {
120466
+ if (error.name === "AbortError") {
120467
+ throw new Error("Connection timeout - endpoint took too long to respond");
120468
+ }
120469
+ throw error;
120470
+ }
120471
+ throw new Error(`Connection failed: ${String(error)}`);
120472
+ }
120473
+ }
120474
+
120475
+ // packages/cli/src/ui/components/AddCustomEndpointDialog.tsx
120424
120476
  import { Fragment as Fragment4, jsx as jsx24, jsxs as jsxs22 } from "react/jsx-runtime";
120425
120477
  function AddCustomEndpointDialog({
120426
120478
  onSubmit,
@@ -120433,11 +120485,25 @@ function AddCustomEndpointDialog({
120433
120485
  const [providerType, setProviderType] = useState33(null);
120434
120486
  const [selectedProviderIndex, setSelectedProviderIndex] = useState33(0);
120435
120487
  const [inputBuffer, setInputBuffer] = useState33("");
120488
+ const [availableModels, setAvailableModels] = useState33([]);
120489
+ const [selectedModelIndex, setSelectedModelIndex] = useState33(0);
120490
+ const [fetchError, setFetchError] = useState33(null);
120436
120491
  const providerOptions = [
120437
120492
  { label: "OpenAI-compatible API", value: AuthType.USE_OPENAI },
120438
120493
  { label: "Ollama", value: AuthType.OLLAMA },
120439
120494
  { label: "LM Studio", value: AuthType.LM_STUDIO }
120440
120495
  ];
120496
+ useEffect32(() => {
120497
+ if (step === "model-fetch" && url2 && providerType) {
120498
+ fetchModelsFromCustomEndpoint(url2, providerType, apiKey || void 0).then((models) => {
120499
+ setAvailableModels(models);
120500
+ setFetchError(null);
120501
+ setStep("model-select");
120502
+ }).catch((err) => {
120503
+ setFetchError(err instanceof Error ? err.message : String(err));
120504
+ });
120505
+ }
120506
+ }, [step, url2, providerType, apiKey]);
120441
120507
  useInput4((input, key) => {
120442
120508
  if (key.escape) {
120443
120509
  onCancel();
@@ -120459,6 +120525,32 @@ function AddCustomEndpointDialog({
120459
120525
  }
120460
120526
  return;
120461
120527
  }
120528
+ if (step === "model-select") {
120529
+ if (key.upArrow && selectedModelIndex > 0) {
120530
+ setSelectedModelIndex(selectedModelIndex - 1);
120531
+ return;
120532
+ }
120533
+ if (key.downArrow && selectedModelIndex < availableModels.length - 1) {
120534
+ setSelectedModelIndex(selectedModelIndex + 1);
120535
+ return;
120536
+ }
120537
+ if (key.return) {
120538
+ if (availableModels.length > 0 && providerType) {
120539
+ const selectedModel = availableModels[selectedModelIndex];
120540
+ onSubmit(name2, url2, selectedModel.id, apiKey, providerType);
120541
+ }
120542
+ return;
120543
+ }
120544
+ return;
120545
+ }
120546
+ if (step === "model-fetch" && fetchError) {
120547
+ if (input === "r" || input === "R") {
120548
+ setFetchError(null);
120549
+ setStep("model-fetch");
120550
+ return;
120551
+ }
120552
+ return;
120553
+ }
120462
120554
  if (key.return) {
120463
120555
  if (step === "name") {
120464
120556
  if (inputBuffer.trim()) {
@@ -120474,9 +120566,8 @@ function AddCustomEndpointDialog({
120474
120566
  }
120475
120567
  } else if (step === "apikey") {
120476
120568
  setApiKey(inputBuffer.trim());
120477
- if (providerType) {
120478
- onSubmit(name2, url2, inputBuffer.trim(), providerType);
120479
- }
120569
+ setInputBuffer("");
120570
+ setStep("model-fetch");
120480
120571
  }
120481
120572
  return;
120482
120573
  }
@@ -120494,7 +120585,7 @@ function AddCustomEndpointDialog({
120494
120585
  return /* @__PURE__ */ jsxs22(Fragment4, { children: [
120495
120586
  /* @__PURE__ */ jsx24(Text26, { bold: true, color: Colors.AccentBlue, children: "Step 1: Endpoint Name" }),
120496
120587
  /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx24(Text26, { children: "Enter a friendly name for this endpoint:" }) }),
120497
- /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx24(Text26, { color: Colors.AccentGreen, children: 'Example: "vLLM Production" or "Internal API"' }) }),
120588
+ /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx24(Text26, { color: Colors.AccentGreen, children: 'Example: "RTX3090 Proxy" or "Inference Server"' }) }),
120498
120589
  /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsxs22(Text26, { children: [
120499
120590
  /* @__PURE__ */ jsx24(Text26, { color: Colors.AccentPurple, children: "> " }),
120500
120591
  inputBuffer,
@@ -120510,7 +120601,7 @@ function AddCustomEndpointDialog({
120510
120601
  /* @__PURE__ */ jsx24(Text26, { color: Colors.AccentGreen, children: name2 })
120511
120602
  ] }) }),
120512
120603
  /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx24(Text26, { children: "Enter the base URL for this endpoint:" }) }),
120513
- /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx24(Text26, { color: Colors.AccentGreen, children: 'Example: "http://localhost:11433" or "https://api.company.com"' }) }),
120604
+ /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx24(Text26, { color: Colors.AccentGreen, children: 'Example: "https://rtx3090.bobai.com.au/v1"' }) }),
120514
120605
  /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsxs22(Text26, { children: [
120515
120606
  /* @__PURE__ */ jsx24(Text26, { color: Colors.AccentPurple, children: "> " }),
120516
120607
  inputBuffer,
@@ -120559,12 +120650,94 @@ function AddCustomEndpointDialog({
120559
120650
  ")"
120560
120651
  ] }) }),
120561
120652
  /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx24(Text26, { children: "Enter API key if required (leave empty if none):" }) }),
120653
+ /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx24(Text26, { color: Colors.AccentYellow, children: "\u2139 External endpoints usually require authentication" }) }),
120562
120654
  /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsxs22(Text26, { children: [
120563
120655
  /* @__PURE__ */ jsx24(Text26, { color: Colors.AccentPurple, children: "> " }),
120564
120656
  "*".repeat(inputBuffer.length),
120565
120657
  /* @__PURE__ */ jsx24(Text26, { color: Colors.AccentCyan, children: "\u258B" })
120566
120658
  ] }) }),
120567
- /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx24(Text26, { color: Colors.Gray, children: "Press Enter to save endpoint, Esc to cancel" }) })
120659
+ /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx24(Text26, { color: Colors.Gray, children: "Press Enter to fetch models, Esc to cancel" }) })
120660
+ ] });
120661
+ case "model-fetch":
120662
+ if (fetchError) {
120663
+ return /* @__PURE__ */ jsxs22(Fragment4, { children: [
120664
+ /* @__PURE__ */ jsx24(Text26, { bold: true, color: Colors.AccentRed, children: "Connection Failed" }),
120665
+ /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsxs22(Text26, { children: [
120666
+ "Endpoint: ",
120667
+ /* @__PURE__ */ jsx24(Text26, { color: Colors.AccentGreen, children: name2 }),
120668
+ " (",
120669
+ url2,
120670
+ ")"
120671
+ ] }) }),
120672
+ /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsxs22(Text26, { color: Colors.AccentRed, children: [
120673
+ "Error: ",
120674
+ fetchError
120675
+ ] }) }),
120676
+ /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx24(Text26, { color: Colors.AccentYellow, children: "Common issues:" }) }),
120677
+ /* @__PURE__ */ jsxs22(Box19, { marginLeft: 2, flexDirection: "column", children: [
120678
+ /* @__PURE__ */ jsx24(Text26, { children: "\u2022 Incorrect URL (verify /v1 suffix)" }),
120679
+ /* @__PURE__ */ jsx24(Text26, { children: "\u2022 API key required but not provided" }),
120680
+ /* @__PURE__ */ jsx24(Text26, { children: "\u2022 Endpoint not accessible from this machine" }),
120681
+ /* @__PURE__ */ jsx24(Text26, { children: "\u2022 Firewall blocking connection" })
120682
+ ] }),
120683
+ /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx24(Text26, { color: Colors.Gray, children: "Press R to retry, Esc to cancel" }) })
120684
+ ] });
120685
+ }
120686
+ return /* @__PURE__ */ jsxs22(Fragment4, { children: [
120687
+ /* @__PURE__ */ jsx24(Text26, { bold: true, color: Colors.AccentBlue, children: "Fetching Models..." }),
120688
+ /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsxs22(Text26, { children: [
120689
+ "Connecting to ",
120690
+ /* @__PURE__ */ jsx24(Text26, { color: Colors.AccentGreen, children: url2 })
120691
+ ] }) }),
120692
+ /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx24(Text26, { color: Colors.Gray, children: "Please wait..." }) })
120693
+ ] });
120694
+ case "model-select":
120695
+ return /* @__PURE__ */ jsxs22(Fragment4, { children: [
120696
+ /* @__PURE__ */ jsx24(Text26, { bold: true, color: Colors.AccentBlue, children: "Step 5: Select Model" }),
120697
+ /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsxs22(Text26, { children: [
120698
+ "Endpoint: ",
120699
+ /* @__PURE__ */ jsx24(Text26, { color: Colors.AccentGreen, children: name2 }),
120700
+ " (",
120701
+ url2,
120702
+ ")"
120703
+ ] }) }),
120704
+ /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsxs22(Text26, { color: Colors.AccentGreen, children: [
120705
+ "\u2713 Found ",
120706
+ availableModels.length,
120707
+ " available models"
120708
+ ] }) }),
120709
+ /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx24(Text26, { children: "Choose a model to use:" }) }),
120710
+ /* @__PURE__ */ jsxs22(Box19, { marginTop: 1, flexDirection: "column", children: [
120711
+ availableModels.slice(0, 15).map((model, index) => {
120712
+ const isSelected = index === selectedModelIndex;
120713
+ const prefix = isSelected ? ">" : " ";
120714
+ return /* @__PURE__ */ jsxs22(
120715
+ Text26,
120716
+ {
120717
+ color: isSelected ? Colors.AccentBlue : Colors.Gray,
120718
+ bold: isSelected,
120719
+ children: [
120720
+ prefix,
120721
+ " ",
120722
+ model.id,
120723
+ " ",
120724
+ /* @__PURE__ */ jsxs22(Text26, { color: Colors.Gray, children: [
120725
+ "(",
120726
+ model.ownedBy,
120727
+ ")"
120728
+ ] })
120729
+ ]
120730
+ },
120731
+ model.id
120732
+ );
120733
+ }),
120734
+ availableModels.length > 15 && /* @__PURE__ */ jsxs22(Text26, { color: Colors.Gray, children: [
120735
+ "... and ",
120736
+ availableModels.length - 15,
120737
+ " more models"
120738
+ ] })
120739
+ ] }),
120740
+ /* @__PURE__ */ jsx24(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx24(Text26, { color: Colors.Gray, children: "\u2191/\u2193 to navigate, Enter to confirm, Esc to cancel" }) })
120568
120741
  ] });
120569
120742
  }
120570
120743
  };
@@ -120588,7 +120761,7 @@ function AddCustomEndpointDialog({
120588
120761
  init_modelManager();
120589
120762
 
120590
120763
  // packages/cli/src/ui/components/OpenAIEndpointDialog.tsx
120591
- import { useState as useState34, useEffect as useEffect32 } from "react";
120764
+ import { useState as useState34, useEffect as useEffect33 } from "react";
120592
120765
  import { Box as Box20, Text as Text27 } from "ink";
120593
120766
  init_database();
120594
120767
  import { jsx as jsx25, jsxs as jsxs23 } from "react/jsx-runtime";
@@ -120618,7 +120791,7 @@ function OpenAIEndpointDialog({
120618
120791
  }) {
120619
120792
  const [savedEndpoints, setSavedEndpoints] = useState34([]);
120620
120793
  const [isLoading, setIsLoading] = useState34(true);
120621
- useEffect32(() => {
120794
+ useEffect33(() => {
120622
120795
  const loadEndpoints = async () => {
120623
120796
  try {
120624
120797
  const db = await getFSSLinkDatabase();
@@ -120827,7 +121000,7 @@ function AuthDialog({
120827
121000
  setSelectedEndpoint(null);
120828
121001
  setErrorMessage("OpenAI endpoint selection is required.");
120829
121002
  };
120830
- const handleCustomEndpointSubmit = async (name2, url2, apiKey, providerType) => {
121003
+ const handleCustomEndpointSubmit = async (name2, url2, model, apiKey, providerType) => {
120831
121004
  const db = await getFSSLinkDatabase();
120832
121005
  const endpointId = await db.saveCustomEndpoint(
120833
121006
  null,
@@ -120839,14 +121012,31 @@ function AuthDialog({
120839
121012
  await db.forceSave();
120840
121013
  console.log(`\u{1F4BE} [AUTH-DIALOG] Custom endpoint saved: ${name2} (${url2})`);
120841
121014
  setShowAddCustomEndpointDialog(false);
120842
- setSelectedEndpoint({ id: endpointId, name: name2, url: url2, isDefault: false });
120843
- if (providerType === AuthType.USE_OPENAI) {
120844
- setShowOpenAIKeyPrompt(true);
120845
- } else if (providerType === AuthType.OLLAMA) {
120846
- setShowOllamaModelPrompt(true);
120847
- } else if (providerType === AuthType.LM_STUDIO) {
120848
- setShowLMStudioModelPrompt(true);
121015
+ const modelManager = getModelManager();
121016
+ const modelId = await modelManager.configureModel(
121017
+ providerType,
121018
+ model,
121019
+ // โœจ Use selected model from dialog
121020
+ url2,
121021
+ // โœจ Use custom endpoint URL
121022
+ apiKey,
121023
+ `${name2}: ${model}`
121024
+ );
121025
+ if (endpointId && modelId) {
121026
+ await db.linkModelToEndpoint(modelId, endpointId);
121027
+ await db.saveCustomEndpoint(
121028
+ modelId,
121029
+ // Set provider_id to the model ID
121030
+ name2,
121031
+ url2,
121032
+ `Custom ${providerType} endpoint`,
121033
+ false
121034
+ );
121035
+ console.log(`\u{1F4BE} [AUTH-DIALOG] Linked custom_endpoints.provider_id=${modelId} for endpoint ${endpointId}`);
121036
+ await db.forceSave();
121037
+ console.log(`\u{1F4BE} [AUTH-DIALOG] Model ${modelId} linked to endpoint ${endpointId}`);
120849
121038
  }
121039
+ onSelect(providerType, "User" /* User */);
120850
121040
  };
120851
121041
  const handleCustomEndpointCancel = () => {
120852
121042
  setShowAddCustomEndpointDialog(false);
@@ -120947,7 +121137,7 @@ function AuthDialog({
120947
121137
  }
120948
121138
 
120949
121139
  // packages/cli/src/ui/components/ProviderEndpointSelectionDialog.tsx
120950
- import { useState as useState36, useEffect as useEffect33 } from "react";
121140
+ import { useState as useState36, useEffect as useEffect34 } from "react";
120951
121141
  import { Box as Box22, Text as Text29, useInput as useInput5 } from "ink";
120952
121142
  init_modelManager();
120953
121143
  import { jsx as jsx27, jsxs as jsxs25 } from "react/jsx-runtime";
@@ -120961,7 +121151,7 @@ function ProviderEndpointSelectionDialog({
120961
121151
  const [loading, setLoading] = useState36(true);
120962
121152
  const [switching, setSwitching] = useState36(false);
120963
121153
  const [error, setError] = useState36(null);
120964
- useEffect33(() => {
121154
+ useEffect34(() => {
120965
121155
  const loadEndpoints = async () => {
120966
121156
  try {
120967
121157
  const modelManager = getModelManager();
@@ -121205,7 +121395,7 @@ function ProviderEndpointSelectionDialog({
121205
121395
  }
121206
121396
 
121207
121397
  // packages/cli/src/ui/components/EnhancedModelSelectionDialog.tsx
121208
- import { useState as useState37, useEffect as useEffect34 } from "react";
121398
+ import { useState as useState37, useEffect as useEffect35 } from "react";
121209
121399
  import { Box as Box23, Text as Text30, useInput as useInput6 } from "ink";
121210
121400
  init_modelManager();
121211
121401
  import { jsx as jsx28, jsxs as jsxs26 } from "react/jsx-runtime";
@@ -121256,7 +121446,7 @@ function EnhancedModelSelectionDialog({
121256
121446
  setLoading(false);
121257
121447
  }
121258
121448
  };
121259
- useEffect34(() => {
121449
+ useEffect35(() => {
121260
121450
  loadModels();
121261
121451
  }, [providerType, endpointUrl]);
121262
121452
  useInput6((input, key) => {
@@ -121596,7 +121786,7 @@ var WelcomeBackDialog = ({
121596
121786
  };
121597
121787
 
121598
121788
  // packages/cli/src/ui/components/SearchEngineConfigDialog.tsx
121599
- import { useState as useState38, useEffect as useEffect35 } from "react";
121789
+ import { useState as useState38, useEffect as useEffect36 } from "react";
121600
121790
  import { Box as Box25, Text as Text32, useInput as useInput7 } from "ink";
121601
121791
  init_database();
121602
121792
  import { jsx as jsx30, jsxs as jsxs28 } from "react/jsx-runtime";
@@ -121609,7 +121799,7 @@ function SearchEngineConfigDialog({
121609
121799
  const [currentField, setCurrentField] = useState38("braveApiKey");
121610
121800
  const [isLoading, setIsLoading] = useState38(true);
121611
121801
  const [isSaving, setIsSaving] = useState38(false);
121612
- useEffect35(() => {
121802
+ useEffect36(() => {
121613
121803
  const loadConfig = async () => {
121614
121804
  console.log("[WEBSCRAPER-DEBUG] Loading existing API keys from database...");
121615
121805
  const db = await getFSSLinkDatabase();
@@ -121629,7 +121819,7 @@ function SearchEngineConfigDialog({
121629
121819
  };
121630
121820
  loadConfig().catch(console.error);
121631
121821
  }, []);
121632
- useEffect35(() => {
121822
+ useEffect36(() => {
121633
121823
  if (!isSaving) return;
121634
121824
  const saveConfig = async () => {
121635
121825
  try {
@@ -121853,7 +122043,7 @@ function SearchEngineConfigDialog({
121853
122043
  }
121854
122044
 
121855
122045
  // packages/cli/src/ui/components/AuthInProgress.tsx
121856
- import { useState as useState39, useEffect as useEffect36 } from "react";
122046
+ import { useState as useState39, useEffect as useEffect37 } from "react";
121857
122047
  import { Box as Box26, Text as Text33 } from "ink";
121858
122048
  import Spinner2 from "ink-spinner";
121859
122049
  import { jsx as jsx31, jsxs as jsxs29 } from "react/jsx-runtime";
@@ -121869,7 +122059,7 @@ function AuthInProgress({
121869
122059
  },
121870
122060
  { isActive: true }
121871
122061
  );
121872
- useEffect36(() => {
122062
+ useEffect37(() => {
121873
122063
  const timer = setTimeout(() => {
121874
122064
  setTimedOut(true);
121875
122065
  onTimeout();
@@ -125480,7 +125670,7 @@ function IdeIntegrationNudge({
125480
125670
  }
125481
125671
 
125482
125672
  // packages/cli/src/ui/hooks/useGitBranchName.ts
125483
- import { useState as useState42, useEffect as useEffect37, useCallback as useCallback24 } from "react";
125673
+ import { useState as useState42, useEffect as useEffect38, useCallback as useCallback24 } from "react";
125484
125674
  import { exec as exec6 } from "node:child_process";
125485
125675
  import fs65 from "node:fs";
125486
125676
  import fsPromises3 from "node:fs/promises";
@@ -125516,7 +125706,7 @@ function useGitBranchName(cwd3) {
125516
125706
  ),
125517
125707
  [cwd3, setBranchName]
125518
125708
  );
125519
- useEffect37(() => {
125709
+ useEffect38(() => {
125520
125710
  fetchBranchName();
125521
125711
  const gitLogsHeadPath = path77.join(cwd3, ".git", "logs", "HEAD");
125522
125712
  let watcher;
@@ -125540,14 +125730,14 @@ function useGitBranchName(cwd3) {
125540
125730
  }
125541
125731
 
125542
125732
  // packages/cli/src/ui/hooks/useBracketedPaste.ts
125543
- import { useEffect as useEffect38 } from "react";
125733
+ import { useEffect as useEffect39 } from "react";
125544
125734
  var ENABLE_BRACKETED_PASTE = "\x1B[?2004h";
125545
125735
  var DISABLE_BRACKETED_PASTE = "\x1B[?2004l";
125546
125736
  var useBracketedPaste = () => {
125547
125737
  const cleanup = () => {
125548
125738
  process.stdout.write(DISABLE_BRACKETED_PASTE);
125549
125739
  };
125550
- useEffect38(() => {
125740
+ useEffect39(() => {
125551
125741
  process.stdout.write(ENABLE_BRACKETED_PASTE);
125552
125742
  process.on("exit", cleanup);
125553
125743
  process.on("SIGINT", cleanup);
@@ -125567,7 +125757,7 @@ import {
125567
125757
  createContext as createContext5,
125568
125758
  useCallback as useCallback25,
125569
125759
  useContext as useContext5,
125570
- useEffect as useEffect39,
125760
+ useEffect as useEffect40,
125571
125761
  useState as useState43
125572
125762
  } from "react";
125573
125763
  import { jsx as jsx59 } from "react/jsx-runtime";
@@ -125581,7 +125771,7 @@ var VimModeProvider = ({
125581
125771
  const [vimMode, setVimMode] = useState43(
125582
125772
  initialVimEnabled ? "NORMAL" : "INSERT"
125583
125773
  );
125584
- useEffect39(() => {
125774
+ useEffect40(() => {
125585
125775
  const enabled = settings.merged.vimMode ?? false;
125586
125776
  setVimEnabled(enabled);
125587
125777
  if (enabled) {
@@ -125614,7 +125804,7 @@ var useVimMode = () => {
125614
125804
  };
125615
125805
 
125616
125806
  // packages/cli/src/ui/hooks/vim.ts
125617
- import { useCallback as useCallback26, useReducer as useReducer4, useEffect as useEffect40 } from "react";
125807
+ import { useCallback as useCallback26, useReducer as useReducer4, useEffect as useEffect41 } from "react";
125618
125808
  var DIGIT_MULTIPLIER = 10;
125619
125809
  var DEFAULT_COUNT = 1;
125620
125810
  var DIGIT_1_TO_9 = /^[1-9]$/;
@@ -125678,7 +125868,7 @@ var vimReducer = (state, action) => {
125678
125868
  function useVim(buffer, onSubmit) {
125679
125869
  const { vimEnabled, vimMode, setVimMode } = useVimMode();
125680
125870
  const [state, dispatch] = useReducer4(vimReducer, initialVimState);
125681
- useEffect40(() => {
125871
+ useEffect41(() => {
125682
125872
  dispatch({ type: "SET_MODE", mode: vimMode });
125683
125873
  }, [vimMode]);
125684
125874
  const updateMode = useCallback26(
@@ -126474,12 +126664,12 @@ import { Box as Box56, Newline as Newline3, Text as Text61 } from "ink";
126474
126664
 
126475
126665
  // packages/cli/src/ui/hooks/usePrivacySettings.ts
126476
126666
  init_dist2();
126477
- import { useState as useState45, useEffect as useEffect41, useCallback as useCallback27 } from "react";
126667
+ import { useState as useState45, useEffect as useEffect42, useCallback as useCallback27 } from "react";
126478
126668
  var usePrivacySettings = (config) => {
126479
126669
  const [privacyState, setPrivacyState] = useState45({
126480
126670
  isLoading: true
126481
126671
  });
126482
- useEffect41(() => {
126672
+ useEffect42(() => {
126483
126673
  const fetchInitialState = async () => {
126484
126674
  setPrivacyState({
126485
126675
  isLoading: true
@@ -126693,7 +126883,7 @@ function useSettingsCommand() {
126693
126883
  }
126694
126884
 
126695
126885
  // packages/cli/src/ui/components/SettingsDialog.tsx
126696
- import React30, { useState as useState47, useEffect as useEffect42 } from "react";
126886
+ import React30, { useState as useState47, useEffect as useEffect43 } from "react";
126697
126887
  import { Box as Box58, Text as Text62 } from "ink";
126698
126888
  init_settings();
126699
126889
  import chalk2 from "chalk";
@@ -126725,7 +126915,7 @@ function SettingsDialog({
126725
126915
  );
126726
126916
  const [globalPendingChanges, setGlobalPendingChanges] = useState47(/* @__PURE__ */ new Map());
126727
126917
  const [_restartRequiredSettings, setRestartRequiredSettings] = useState47(/* @__PURE__ */ new Set());
126728
- useEffect42(() => {
126918
+ useEffect43(() => {
126729
126919
  let updated = structuredClone(settings.forScope(selectedScope).settings);
126730
126920
  const newModified = /* @__PURE__ */ new Set();
126731
126921
  const newRestartRequired = /* @__PURE__ */ new Set();
@@ -126835,7 +127025,7 @@ function SettingsDialog({
126835
127025
  const [editBuffer, setEditBuffer] = useState47("");
126836
127026
  const [editCursorPos, setEditCursorPos] = useState47(0);
126837
127027
  const [cursorVisible, setCursorVisible] = useState47(true);
126838
- useEffect42(() => {
127028
+ useEffect43(() => {
126839
127029
  if (!editingKey) {
126840
127030
  setCursorVisible(true);
126841
127031
  return;
@@ -127539,11 +127729,11 @@ var App = ({ config, settings, startupWarnings = [], version }) => {
127539
127729
  const { history, addItem, clearItems, loadHistory } = useHistory();
127540
127730
  const [idePromptAnswered, setIdePromptAnswered] = useState48(false);
127541
127731
  const currentIDE = config.getIdeClient().getCurrentIde();
127542
- useEffect43(() => {
127732
+ useEffect44(() => {
127543
127733
  registerCleanup(() => config.getIdeClient().disconnect());
127544
127734
  }, [config]);
127545
127735
  const shouldShowIdePrompt = currentIDE && !config.getIdeMode() && !settings.merged.hasSeenIdeIntegrationNudge && !idePromptAnswered;
127546
- useEffect43(() => {
127736
+ useEffect44(() => {
127547
127737
  const cleanup = setUpdateHandler(addItem, setUpdateInfo);
127548
127738
  return cleanup;
127549
127739
  }, [addItem]);
@@ -127552,7 +127742,7 @@ var App = ({ config, settings, startupWarnings = [], version }) => {
127552
127742
  handleNewMessage,
127553
127743
  clearConsoleMessages: clearConsoleMessagesState
127554
127744
  } = useConsoleMessages();
127555
- useEffect43(() => {
127745
+ useEffect44(() => {
127556
127746
  const consolePatcher = new ConsolePatcher({
127557
127747
  onNewMessage: handleNewMessage,
127558
127748
  debugMode: config.getDebugMode()
@@ -127575,7 +127765,7 @@ var App = ({ config, settings, startupWarnings = [], version }) => {
127575
127765
  const [isSearchDialogOpen, setIsSearchDialogOpen] = useState48(false);
127576
127766
  const [isAddCustomEndpointDialogOpen, setIsAddCustomEndpointDialogOpen] = useState48(false);
127577
127767
  const [footerHeight, setFooterHeight] = useState48(0);
127578
- useEffect43(() => {
127768
+ useEffect44(() => {
127579
127769
  const initializeSearchEngineConfig = async () => {
127580
127770
  const configProvider = SearchEngineConfigProvider.getInstance();
127581
127771
  await configProvider.loadConfiguration();
@@ -127602,12 +127792,12 @@ var App = ({ config, settings, startupWarnings = [], version }) => {
127602
127792
  const [ideContextState, setIdeContextState] = useState48();
127603
127793
  const [showEscapePrompt, setShowEscapePrompt] = useState48(false);
127604
127794
  const [isProcessing, setIsProcessing] = useState48(false);
127605
- useEffect43(() => {
127795
+ useEffect44(() => {
127606
127796
  const unsubscribe = ideContext.subscribeToIdeContext(setIdeContextState);
127607
127797
  setIdeContextState(ideContext.getIdeContext());
127608
127798
  return unsubscribe;
127609
127799
  }, []);
127610
- useEffect43(() => {
127800
+ useEffect44(() => {
127611
127801
  const openDebugConsole = () => {
127612
127802
  setShowErrorDetails(true);
127613
127803
  setConstrainHeight(false);
@@ -127674,7 +127864,7 @@ var App = ({ config, settings, startupWarnings = [], version }) => {
127674
127864
  handleRestart: handleWelcomeBackRestart,
127675
127865
  handleCancel: handleWelcomeBackCancel
127676
127866
  } = useWelcomeBack(settings);
127677
- useEffect43(() => {
127867
+ useEffect44(() => {
127678
127868
  if (settings.merged.selectedAuthType && !settings.merged.useExternalAuth) {
127679
127869
  const error = validateAuthMethod(settings.merged.selectedAuthType);
127680
127870
  if (error) {
@@ -127688,7 +127878,7 @@ var App = ({ config, settings, startupWarnings = [], version }) => {
127688
127878
  openAuthDialog,
127689
127879
  setAuthError
127690
127880
  ]);
127691
- useEffect43(() => {
127881
+ useEffect44(() => {
127692
127882
  if (!isAuthenticating) {
127693
127883
  setUserTier(config.getGeminiClient()?.getUserTier());
127694
127884
  }
@@ -127752,7 +127942,7 @@ var App = ({ config, settings, startupWarnings = [], version }) => {
127752
127942
  console.error("Error refreshing memory:", error);
127753
127943
  }
127754
127944
  }, [config, addItem, settings.merged]);
127755
- useEffect43(() => {
127945
+ useEffect44(() => {
127756
127946
  const checkModelChange = () => {
127757
127947
  const configModel = config.getModel();
127758
127948
  if (configModel !== currentModel) {
@@ -127763,7 +127953,7 @@ var App = ({ config, settings, startupWarnings = [], version }) => {
127763
127953
  const interval = setInterval(checkModelChange, 1e3);
127764
127954
  return () => clearInterval(interval);
127765
127955
  }, [config, currentModel]);
127766
- useEffect43(() => {
127956
+ useEffect44(() => {
127767
127957
  const flashFallbackHandler = async (currentModel2, fallbackModel, error) => {
127768
127958
  let message;
127769
127959
  if (config.getContentGeneratorConfig().authType === AuthType.LOGIN_WITH_GOOGLE) {
@@ -127922,7 +128112,7 @@ var App = ({ config, settings, startupWarnings = [], version }) => {
127922
128112
  refreshStatic,
127923
128113
  () => cancelHandlerRef.current()
127924
128114
  );
127925
- useEffect43(() => {
128115
+ useEffect44(() => {
127926
128116
  if (contextToPopulate && !initialPromptSubmitted.current) {
127927
128117
  submitQuery(contextToPopulate);
127928
128118
  initialPromptSubmitted.current = true;
@@ -128057,13 +128247,13 @@ ${queuedText}` : queuedText;
128057
128247
  useKeypress(handleGlobalKeypress, {
128058
128248
  isActive: true
128059
128249
  });
128060
- useEffect43(() => {
128250
+ useEffect44(() => {
128061
128251
  if (config) {
128062
128252
  setGeminiMdFileCount(config.getGeminiMdFileCount());
128063
128253
  }
128064
128254
  }, [config, config.getGeminiMdFileCount]);
128065
128255
  const logger6 = useLogger();
128066
- useEffect43(() => {
128256
+ useEffect44(() => {
128067
128257
  const fetchUserMessages = async () => {
128068
128258
  const pastMessagesRaw = await logger6?.getPreviousUserMessages() || [];
128069
128259
  const currentSessionUserMessages = history.filter(
@@ -128095,7 +128285,7 @@ ${queuedText}` : queuedText;
128095
128285
  }, [clearItems, clearConsoleMessagesState, refreshStatic]);
128096
128286
  const mainControlsRef = useRef12(null);
128097
128287
  const pendingHistoryItemRef = useRef12(null);
128098
- useEffect43(() => {
128288
+ useEffect44(() => {
128099
128289
  if (mainControlsRef.current) {
128100
128290
  const fullFooterMeasurement = measureElement(mainControlsRef.current);
128101
128291
  setFooterHeight(fullFooterMeasurement.height);
@@ -128109,7 +128299,7 @@ ${queuedText}` : queuedText;
128109
128299
  () => terminalHeight - footerHeight - staticExtraHeight,
128110
128300
  [terminalHeight, footerHeight]
128111
128301
  );
128112
- useEffect43(() => {
128302
+ useEffect44(() => {
128113
128303
  if (isInitialMount.current) {
128114
128304
  isInitialMount.current = false;
128115
128305
  return;
@@ -128122,7 +128312,7 @@ ${queuedText}` : queuedText;
128122
128312
  clearTimeout(handler);
128123
128313
  };
128124
128314
  }, [terminalWidth, terminalHeight, refreshStatic]);
128125
- useEffect43(() => {
128315
+ useEffect44(() => {
128126
128316
  if (streamingState === "idle" /* Idle */ && staticNeedsRefresh) {
128127
128317
  setStaticNeedsRefresh(false);
128128
128318
  refreshStatic();
@@ -128144,7 +128334,7 @@ ${queuedText}` : queuedText;
128144
128334
  }, [settings.merged.contextFileName]);
128145
128335
  const initialPrompt = useMemo9(() => config.getQuestion(), [config]);
128146
128336
  const geminiClient = config.getGeminiClient();
128147
- useEffect43(() => {
128337
+ useEffect44(() => {
128148
128338
  if (initialPrompt && !initialPromptSubmitted.current && !isAuthenticating && !isAuthDialogOpen && !shouldShowWelcomeBack && !isThemeDialogOpen && !isEditorDialogOpen && !isSearchDialogOpen && modelDialogState === "none" && !showPrivacyNotice && geminiClient?.isInitialized?.()) {
128149
128339
  submitQuery(initialPrompt);
128150
128340
  initialPromptSubmitted.current = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fss-link",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "engines": {
5
5
  "node": ">=20.0.0"
6
6
  },