llmwiki-bridge-start 0.0.1 → 0.0.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.
- package/README.md +22 -3
- package/package.json +1 -1
- package/src/index.mjs +789 -53
package/README.md
CHANGED
|
@@ -35,6 +35,14 @@ scripts that support MCP over HTTP; exact client configuration syntax varies by
|
|
|
35
35
|
client. `llmwiki-agent-bridge` can still be added later when you want source
|
|
36
36
|
fan-out or one normalized bridge across sources.
|
|
37
37
|
|
|
38
|
+
After a quickstart run, inspect what is still running and whether each source
|
|
39
|
+
is registered with the bridge:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx llmwiki-bridge-start@latest status --bridge http://127.0.0.1:8788
|
|
43
|
+
npx llmwiki-bridge-start@latest status --json
|
|
44
|
+
```
|
|
45
|
+
|
|
38
46
|
If `llmwiki-serve` is not on `PATH`, a sibling `../llmwiki-serve` checkout with
|
|
39
47
|
an existing `.venv` is used automatically when available. Otherwise point the
|
|
40
48
|
harness at a local checkout or environment explicitly, for example:
|
|
@@ -71,7 +79,7 @@ transcripts are unchanged. Use `--no-clear-screen` or
|
|
|
71
79
|
visible. If
|
|
72
80
|
you opt in, quickstart uses an already running bridge or prints copy-pasteable
|
|
73
81
|
PowerShell and POSIX manual-start examples such as
|
|
74
|
-
`LLMWIKI_AGENT_BRIDGE_HOST='127.0.0.1' LLMWIKI_AGENT_BRIDGE_PORT='8788' npx --yes llmwiki-agent-bridge@0.
|
|
82
|
+
`LLMWIKI_AGENT_BRIDGE_HOST='127.0.0.1' LLMWIKI_AGENT_BRIDGE_PORT='8788' npx --yes llmwiki-agent-bridge@0.3.0`;
|
|
75
83
|
custom `--bridge http://host:port` values are reflected in those env
|
|
76
84
|
assignments. The command does not install a global package.
|
|
77
85
|
Before starting that bridge command, quickstart asks how to configure the LLM
|
|
@@ -100,6 +108,12 @@ endpoint is shown, it came from an explicit CLI flag, the standard
|
|
|
100
108
|
such as Hermes `/health`; blank Enter uses that verified/configured default,
|
|
101
109
|
and `skip` forces evidence-only. If no default endpoint is shown, blank Enter
|
|
102
110
|
or `skip` continues with evidence-only bridge smoke.
|
|
111
|
+
Configured and reachable are separate states. Quickstart and delegated-runtime
|
|
112
|
+
smoke do not treat an endpoint as usable just because a default or settings
|
|
113
|
+
value exists. Hermes endpoints must answer `/health` or `/v1/health`; generic
|
|
114
|
+
OpenAI-compatible endpoints must answer a minimal `/models` probe. Evidence-only
|
|
115
|
+
smoke explicitly skips the delegated-runtime check and verifies source
|
|
116
|
+
retrieval only.
|
|
103
117
|
DeepAgents ACP is an explicit bridge adapter path, not the default DeepAgents
|
|
104
118
|
profile behavior. Use `--runtime-adapter deepagents-acp` when you want
|
|
105
119
|
QuickStart to configure `runtimeAdapter=deepagents-acp`; a background bridge
|
|
@@ -119,7 +133,10 @@ endpoint (`POST /message:send`), MCP-style JSON-RPC endpoint (`POST /mcp`),
|
|
|
119
133
|
settings UI (`/settings`), and a compact operational details section that
|
|
120
134
|
points to `.llmwiki-bridge-start/quickstart-handoff.md` for PIDs, log paths,
|
|
121
135
|
and stop guidance. The bridge endpoint handoff is separate from direct source
|
|
122
|
-
MCP Streamable HTTP URLs (`/mcp/stream`).
|
|
136
|
+
MCP Streamable HTTP URLs (`/mcp/stream`). Use an `llmwiki-agent-bridge` build
|
|
137
|
+
with MCP lifecycle support when registering the bridge `POST /mcp` endpoint
|
|
138
|
+
with spec-compliant MCP clients; direct source URLs continue to use
|
|
139
|
+
`/mcp/stream`. If an explicit LLM
|
|
123
140
|
endpoint is configured through flags, quickstart treats it as a preconfigured
|
|
124
141
|
compatibility path without adding it to the interactive first-run menu.
|
|
125
142
|
|
|
@@ -179,6 +196,7 @@ npx llmwiki-bridge-start@latest discover --home
|
|
|
179
196
|
npx llmwiki-bridge-start@latest discover --path . --validate
|
|
180
197
|
npx llmwiki-bridge-start@latest start --path ./wiki --port 11001
|
|
181
198
|
npx llmwiki-bridge-start@latest register --bridge http://127.0.0.1:8788 --config .llmwiki-bridge-start/sources.json
|
|
199
|
+
npx llmwiki-bridge-start@latest status --bridge http://127.0.0.1:8788
|
|
182
200
|
npx llmwiki-bridge-start@latest smoke --bridge http://127.0.0.1:8788
|
|
183
201
|
```
|
|
184
202
|
|
|
@@ -217,7 +235,8 @@ longer compatibility notes and non-goals.
|
|
|
217
235
|
| `discover` | Find local LLMWiki Markdown, Native LLMWiki/OpenWiki, Obsidian, Logseq, Foam, Dendron, Quartz, and generic Markdown candidates. |
|
|
218
236
|
| `start` | Start `llmwiki-serve` on one or more selected local wiki paths. |
|
|
219
237
|
| `register` | Upsert started or existing source URLs into `llmwiki-agent-bridge`. Use `--replace` only when intentionally replacing the registry. |
|
|
220
|
-
| `
|
|
238
|
+
| `status` / `ls` | List local started sources from `.llmwiki-bridge-start/sources.json`, live source health, and bridge registration state when the bridge is reachable. |
|
|
239
|
+
| `smoke` | Run a bridge smoke request; defaults to evidence-only and accepts delegated-runtime or hybrid with `--mode`. Evidence-only reports that delegated-runtime was not checked; delegated-runtime verifies runtime reachability first. |
|
|
221
240
|
| `doctor` | Check local prerequisites and optional bridge reachability. |
|
|
222
241
|
|
|
223
242
|
## Boundary
|
package/package.json
CHANGED
package/src/index.mjs
CHANGED
|
@@ -23,11 +23,14 @@ const FAST_DISCOVERY_MAX_BUFFER = 32 * 1024 * 1024
|
|
|
23
23
|
const ASYNC_DISCOVERY_YIELD_EVERY = 64
|
|
24
24
|
const DEFAULT_SOURCE_HEALTH_TIMEOUT_MS = 15000
|
|
25
25
|
const DEFAULT_SOURCE_HEALTH_INTERVAL_MS = 500
|
|
26
|
+
const DEFAULT_STATUS_SOURCE_HEALTH_TIMEOUT_MS = 2000
|
|
27
|
+
const DEFAULT_STATUS_BRIDGE_TIMEOUT_MS = 3000
|
|
28
|
+
const DEFAULT_RUNTIME_REACHABILITY_TIMEOUT_MS = 2000
|
|
26
29
|
const SOURCE_PORT_PROBE_MAX_ATTEMPTS = 200
|
|
27
30
|
const DEFAULT_DISCOVERY_PROGRESS_INTERVAL_MS = 1000
|
|
28
31
|
const DEFAULT_DISCOVERY_PROGRESS_MESSAGE = 'Searching local folders for LLMWiki candidates...'
|
|
29
32
|
const DEFAULT_TERMINAL_ROW_FALLBACK = 1000
|
|
30
|
-
const DEFAULT_BRIDGE_PACKAGE_SPEC = 'llmwiki-agent-bridge@0.
|
|
33
|
+
const DEFAULT_BRIDGE_PACKAGE_SPEC = 'llmwiki-agent-bridge@0.3.0'
|
|
31
34
|
const DEFAULT_BRIDGE_RUNTIME_BASE_URL = 'http://127.0.0.1:8642/v1'
|
|
32
35
|
const DEFAULT_RUNTIME_FRAMEWORK_DETECTION_TIMEOUT_MS = 1500
|
|
33
36
|
const DEFAULT_RUNTIME_INSTALL_TIMEOUT_MS = 10 * 60 * 1000
|
|
@@ -410,6 +413,14 @@ export async function runCli(argv, io = { stdin: process.stdin, stdout: process.
|
|
|
410
413
|
writeResult(result, options, io)
|
|
411
414
|
return
|
|
412
415
|
}
|
|
416
|
+
if (command === 'status' || command === 'ls') {
|
|
417
|
+
const result = await statusSources({
|
|
418
|
+
bridgeUrl: stringOption(options.bridge, DEFAULT_BRIDGE_URL),
|
|
419
|
+
configPath: stringOption(options.config, defaultConfigPath()),
|
|
420
|
+
})
|
|
421
|
+
writeResult(result, options, io)
|
|
422
|
+
return
|
|
423
|
+
}
|
|
413
424
|
if (command === 'start') {
|
|
414
425
|
const result = await startSources({
|
|
415
426
|
paths: arrayOption(options.path),
|
|
@@ -440,6 +451,8 @@ export async function runCli(argv, io = { stdin: process.stdin, stdout: process.
|
|
|
440
451
|
bridgeUrl: stringOption(options.bridge, DEFAULT_BRIDGE_URL),
|
|
441
452
|
query: stringOption(options.query, 'What LLMWiki sources are available and what are they for?'),
|
|
442
453
|
mode: bridgeModeOption(options.mode ?? options['orchestration-mode'], BRIDGE_MODE_EVIDENCE_ONLY),
|
|
454
|
+
runtime: detectLlmRuntime(options, process.env),
|
|
455
|
+
preflightRuntime: true,
|
|
443
456
|
})
|
|
444
457
|
writeResult(result, options, io)
|
|
445
458
|
return
|
|
@@ -567,6 +580,7 @@ export async function quickstart(options = {}, io = { stdin: process.stdin, stdo
|
|
|
567
580
|
output.write(formatSelectedSourceEcho(selected, candidatePlan.visibleCandidates))
|
|
568
581
|
writeQuickstartStep(output, ui, 3, QUICKSTART_STEP_TOTAL, 'Validate and start local sources')
|
|
569
582
|
}
|
|
583
|
+
assertNoSelectedSourcePathConflicts(selected, { action: 'quickstart source selection' })
|
|
570
584
|
if (!await confirmQuickstart(prompter, `Start ${selected.length} selected source server(s) on loopback?\nThis validates each selected folder first.`, true)) {
|
|
571
585
|
writeStatus(output, ui, 'skip', 'Skipped source startup. No source servers were started.')
|
|
572
586
|
result.skipped.push('start', 'bridge-setup', 'register', 'smoke')
|
|
@@ -598,6 +612,7 @@ export async function quickstart(options = {}, io = { stdin: process.stdin, stdo
|
|
|
598
612
|
result.skipped.push('start', 'bridge-setup', 'register', 'smoke')
|
|
599
613
|
return result
|
|
600
614
|
}
|
|
615
|
+
assertNoValidatedSourceIdConflicts(startable, { action: 'quickstart source selection' })
|
|
601
616
|
result.started = await runtime.startSources({
|
|
602
617
|
paths: startable.map((candidate) => candidate.path),
|
|
603
618
|
host: stringOption(options.host, DEFAULT_HOST),
|
|
@@ -681,13 +696,23 @@ export async function quickstart(options = {}, io = { stdin: process.stdin, stdo
|
|
|
681
696
|
})
|
|
682
697
|
writeStatus(output, ui, 'ok', formatQuickstartRegistrationSuccess(result.registered, quickstartSelectedIds))
|
|
683
698
|
|
|
684
|
-
const smokePlan = await runtime.selectBridgeSmokeMode({
|
|
699
|
+
const smokePlan = await runtime.selectBridgeSmokeMode({
|
|
700
|
+
options: smokeOptions,
|
|
701
|
+
bridgeUrl,
|
|
702
|
+
env: process.env,
|
|
703
|
+
probeRuntime: runtime.probeRuntimeEndpoint || probeRuntimeEndpoint,
|
|
704
|
+
})
|
|
685
705
|
result.smokeMode = smokePlan.mode
|
|
706
|
+
if (smokePlan.mode === BRIDGE_MODE_EVIDENCE_ONLY) {
|
|
707
|
+
writeStatus(output, ui, 'info', 'Evidence-only smoke checks source retrieval only; delegated-runtime reachability was not checked.')
|
|
708
|
+
}
|
|
686
709
|
writeStatus(output, ui, 'run', `Running bridge smoke in ${formatBridgeModeLabel(smokePlan.mode)} mode (${smokePlan.reason}).`)
|
|
687
710
|
result.smoked = await runtime.smokeBridge({
|
|
688
711
|
bridgeUrl,
|
|
689
712
|
query: stringOption(options.query, 'What LLMWiki sources are available and what are they for?'),
|
|
690
713
|
mode: smokePlan.mode,
|
|
714
|
+
runtime: detectLlmRuntime(smokeOptions, process.env),
|
|
715
|
+
preflightRuntime: smokePlan.mode !== BRIDGE_MODE_EVIDENCE_ONLY,
|
|
691
716
|
})
|
|
692
717
|
writeStatus(output, ui, 'ok', `Smoke complete: ${result.smoked.status?.state || result.smoked.status?.message?.kind || 'ok'}`)
|
|
693
718
|
result.runSummary = writeQuickstartRunSummary({
|
|
@@ -700,7 +725,7 @@ export async function quickstart(options = {}, io = { stdin: process.stdin, stdo
|
|
|
700
725
|
smoked: result.smoked,
|
|
701
726
|
mode: 'bridge',
|
|
702
727
|
})
|
|
703
|
-
output.write(formatBridgeHandoff(bridgeUrl))
|
|
728
|
+
output.write(formatBridgeHandoff(bridgeUrl, { smokeMode: result.smokeMode }))
|
|
704
729
|
output.write(formatLocalProcessLifecycleNote({ sources: result.started.sources, bridgeSetup: result.bridgeSetup, mode: 'bridge', runSummary: result.runSummary }))
|
|
705
730
|
return result
|
|
706
731
|
} finally {
|
|
@@ -765,11 +790,24 @@ async function guideRuntimeSetup({ prompter, output, ui, options, runtime, io =
|
|
|
765
790
|
if (!requestedChoice) {
|
|
766
791
|
const preconfigured = detectLlmRuntime(options, {})
|
|
767
792
|
if (preconfigured.configured) {
|
|
768
|
-
writeStatus(output, ui, 'ok', `Using preconfigured LLM runtime from explicit flags: ${formatRuntimeDetails(preconfigured)}`)
|
|
769
793
|
const choice = preconfigured.runtimeAdapter === RUNTIME_ADAPTER_DEEPAGENTS_ACP
|
|
770
794
|
? runtimeSetupChoiceById(RUNTIME_SETUP_DEEPAGENTS)
|
|
771
795
|
: runtimeSetupChoiceById(RUNTIME_SETUP_EXISTING)
|
|
772
|
-
|
|
796
|
+
writeStatus(output, ui, 'info', `Checking preconfigured LLM runtime from explicit flags: ${formatRuntimeDetails(preconfigured)}`)
|
|
797
|
+
const readiness = await verifyPreconfiguredRuntimeSetup({
|
|
798
|
+
choice,
|
|
799
|
+
runtimeInfo: preconfigured,
|
|
800
|
+
output,
|
|
801
|
+
ui,
|
|
802
|
+
probe: runtime.probeRuntimeEndpoint || probeRuntimeEndpoint,
|
|
803
|
+
})
|
|
804
|
+
if (!readiness.ok) {
|
|
805
|
+
const nextAction = `Start the configured runtime endpoint, verify it responds, then rerun with --llm-endpoint ${preconfigured.baseUrl || '<runtime-url>'} --llm-model ${preconfigured.model} --runtime-profile ${preconfigured.profile}, or use bridge /settings.`
|
|
806
|
+
writeStatus(output, ui, 'skip', `Preconfigured runtime was not reachable. Continuing with evidence-only bridge mode. Next action: ${nextAction}`)
|
|
807
|
+
return unconfiguredRuntimeSetup(choice, nextAction)
|
|
808
|
+
}
|
|
809
|
+
writeStatus(output, ui, 'ok', `Using reachable preconfigured LLM runtime from explicit flags: ${formatRuntimeDetails(preconfigured)}`)
|
|
810
|
+
return configuredRuntimeSetup(choice, { ...preconfigured, reachable: true, reachability: readiness }, runtimeOptionsFromRuntimeInfo(choice, preconfigured))
|
|
773
811
|
}
|
|
774
812
|
}
|
|
775
813
|
|
|
@@ -1482,7 +1520,11 @@ async function promptRuntimeConnection({ prompter, output, ui, options, choice,
|
|
|
1482
1520
|
runtimeProfile: profile || choice.profile,
|
|
1483
1521
|
...(runtimeAdapter ? { runtimeAdapter } : {}),
|
|
1484
1522
|
}
|
|
1485
|
-
const runtimeInfo =
|
|
1523
|
+
const runtimeInfo = {
|
|
1524
|
+
...detectLlmRuntime(runtimeOptions, {}),
|
|
1525
|
+
reachable: true,
|
|
1526
|
+
reachability: endpointProbe,
|
|
1527
|
+
}
|
|
1486
1528
|
writeStatus(output, ui, 'ok', `Runtime configured for bridge start: ${formatRuntimeDetails(runtimeInfo)}`)
|
|
1487
1529
|
return {
|
|
1488
1530
|
choice: choice.id,
|
|
@@ -1590,15 +1632,14 @@ function writeRuntimeEndpointDefaultNotice(output, ui, choice, endpointDefault =
|
|
|
1590
1632
|
}
|
|
1591
1633
|
|
|
1592
1634
|
async function verifyRuntimeEndpointForChoice({ choice, baseUrl, output, ui, probe = probeRuntimeEndpoint } = {}) {
|
|
1593
|
-
if (choice.id !== RUNTIME_SETUP_HERMES) {
|
|
1594
|
-
return { ok: true, skipped: true, reason: `${choice.label} has no registered framework health probe in quickstart yet` }
|
|
1595
|
-
}
|
|
1596
1635
|
const health = await probe({ baseUrl, profile: choice.profile })
|
|
1597
1636
|
if (health.ok) {
|
|
1598
|
-
|
|
1637
|
+
const checkName = choice.id === RUNTIME_SETUP_HERMES || choice.profile === RUNTIME_SETUP_HERMES ? 'health check' : 'OpenAI-compatible /models check'
|
|
1638
|
+
writeStatus(output, ui, 'ok', `${choice.label} ${checkName} passed: ${health.url}`)
|
|
1599
1639
|
return health
|
|
1600
1640
|
}
|
|
1601
|
-
|
|
1641
|
+
const checkName = choice.id === RUNTIME_SETUP_HERMES || choice.profile === RUNTIME_SETUP_HERMES ? 'health check' : 'OpenAI-compatible /models check'
|
|
1642
|
+
writeStatus(output, ui, 'fail', `${choice.label} ${checkName} failed for ${baseUrl}: ${health.error || 'unreachable'}`)
|
|
1602
1643
|
return health
|
|
1603
1644
|
}
|
|
1604
1645
|
|
|
@@ -1742,6 +1783,8 @@ function configuredRuntimeSetup(choice, runtime, runtimeOptions = {}) {
|
|
|
1742
1783
|
model: runtime.model,
|
|
1743
1784
|
profile: runtime.profile,
|
|
1744
1785
|
runtimeAdapter: runtime.runtimeAdapter || '',
|
|
1786
|
+
reachable: runtime.reachable === true || runtime.reachability?.ok === true,
|
|
1787
|
+
reachability: runtime.reachability,
|
|
1745
1788
|
runtimeOptions,
|
|
1746
1789
|
runtime,
|
|
1747
1790
|
}
|
|
@@ -1759,6 +1802,22 @@ function configuredDeepAgentsAcpRuntimeSetup({ choice, options = {}, output, ui
|
|
|
1759
1802
|
return configuredRuntimeSetup(choice, runtimeInfo, runtimeOptions)
|
|
1760
1803
|
}
|
|
1761
1804
|
|
|
1805
|
+
async function verifyPreconfiguredRuntimeSetup({ choice, runtimeInfo, output, ui, probe = probeRuntimeEndpoint } = {}) {
|
|
1806
|
+
if (runtimeInfo.runtimeAdapter === RUNTIME_ADAPTER_DEEPAGENTS_ACP && !runtimeInfo.baseUrl) {
|
|
1807
|
+
return { ok: true, skipped: true, reason: 'adapter-only runtime path' }
|
|
1808
|
+
}
|
|
1809
|
+
if (!runtimeInfo.baseUrl) {
|
|
1810
|
+
return { ok: false, error: 'no runtime base URL configured' }
|
|
1811
|
+
}
|
|
1812
|
+
return verifyRuntimeEndpointForChoice({
|
|
1813
|
+
choice: { ...choice, profile: runtimeInfo.profile, label: choice.label || runtimeInfo.profile },
|
|
1814
|
+
baseUrl: runtimeInfo.baseUrl,
|
|
1815
|
+
output,
|
|
1816
|
+
ui,
|
|
1817
|
+
probe,
|
|
1818
|
+
})
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1762
1821
|
function runtimeOptionsFromRuntimeInfo(choice, runtime) {
|
|
1763
1822
|
return {
|
|
1764
1823
|
runtimeSetup: choice.id,
|
|
@@ -2245,6 +2304,100 @@ function sourceUrlsFromStartedSources(sources = []) {
|
|
|
2245
2304
|
return sources.map((source) => source.url).filter(Boolean)
|
|
2246
2305
|
}
|
|
2247
2306
|
|
|
2307
|
+
function assertNoSelectedSourcePathConflicts(sources = [], { action = 'source selection' } = {}) {
|
|
2308
|
+
const conflicts = selectedSourcePathConflicts(sources)
|
|
2309
|
+
if (conflicts.length) {
|
|
2310
|
+
throw new Error(formatSourceConflictError(conflicts, {
|
|
2311
|
+
action,
|
|
2312
|
+
intro: 'Selected source folders overlap by ancestor path.',
|
|
2313
|
+
remediation: 'Select only one folder from each overlap group, then rerun. If both folders must be served, rebuild one source with a distinct source_id and start it separately.',
|
|
2314
|
+
}))
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
function assertNoValidatedSourceIdConflicts(sources = [], { action = 'source startup' } = {}) {
|
|
2319
|
+
const conflicts = selectedSourceIdConflicts(sources)
|
|
2320
|
+
if (conflicts.length) {
|
|
2321
|
+
throw new Error(formatSourceConflictError(conflicts, {
|
|
2322
|
+
action,
|
|
2323
|
+
intro: 'Selected source folders resolve to duplicate source_id values.',
|
|
2324
|
+
remediation: 'Select only one folder for each source_id, or rebuild/rename one source so every started source has a unique source_id.',
|
|
2325
|
+
}))
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
function selectedSourcePathConflicts(sources = []) {
|
|
2330
|
+
const entries = sources
|
|
2331
|
+
.map((source) => ({ source, path: source.path ? resolve(source.path) : '' }))
|
|
2332
|
+
.filter((entry) => entry.path)
|
|
2333
|
+
const conflicts = []
|
|
2334
|
+
for (let leftIndex = 0; leftIndex < entries.length; leftIndex += 1) {
|
|
2335
|
+
for (let rightIndex = leftIndex + 1; rightIndex < entries.length; rightIndex += 1) {
|
|
2336
|
+
const left = entries[leftIndex]
|
|
2337
|
+
const right = entries[rightIndex]
|
|
2338
|
+
if (pathsOverlapAsAncestor(left.path, right.path)) {
|
|
2339
|
+
conflicts.push({
|
|
2340
|
+
code: 'overlapping_source_paths',
|
|
2341
|
+
detail: 'ancestor/descendant paths would create ambiguous bridge ownership',
|
|
2342
|
+
sources: [left.source, right.source],
|
|
2343
|
+
})
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
return conflicts
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
function selectedSourceIdConflicts(sources = []) {
|
|
2351
|
+
const groups = new Map()
|
|
2352
|
+
for (const source of sources) {
|
|
2353
|
+
const id = sourceIdForConflictCheck(source)
|
|
2354
|
+
if (!id) {
|
|
2355
|
+
continue
|
|
2356
|
+
}
|
|
2357
|
+
const group = groups.get(id) || []
|
|
2358
|
+
group.push(source)
|
|
2359
|
+
groups.set(id, group)
|
|
2360
|
+
}
|
|
2361
|
+
return [...groups.entries()]
|
|
2362
|
+
.filter(([, group]) => group.length > 1)
|
|
2363
|
+
.map(([id, group]) => ({
|
|
2364
|
+
code: 'duplicate_source_id',
|
|
2365
|
+
detail: `duplicate source_id "${id}"`,
|
|
2366
|
+
sources: group,
|
|
2367
|
+
}))
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
function sourceIdForConflictCheck(source = {}) {
|
|
2371
|
+
return String(source.id || source.sourceId || source.source_id || source.manifest?.source_id || '').trim()
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
function pathsOverlapAsAncestor(leftPath, rightPath) {
|
|
2375
|
+
const left = normalizePath(leftPath)
|
|
2376
|
+
const right = normalizePath(rightPath)
|
|
2377
|
+
if (!left || !right || left === right) {
|
|
2378
|
+
return false
|
|
2379
|
+
}
|
|
2380
|
+
return isPathAtOrBelowRoot(left, right) || isPathAtOrBelowRoot(right, left)
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
function formatSourceConflictError(conflicts = [], { action, intro, remediation } = {}) {
|
|
2384
|
+
const lines = [
|
|
2385
|
+
`Cannot continue ${action}: ${intro}`,
|
|
2386
|
+
]
|
|
2387
|
+
for (const conflict of conflicts) {
|
|
2388
|
+
lines.push(`- ${conflict.code}: ${conflict.detail}`)
|
|
2389
|
+
for (const source of conflict.sources || []) {
|
|
2390
|
+
const id = sourceIdForConflictCheck(source)
|
|
2391
|
+
const idText = id ? ` id=${id}` : ''
|
|
2392
|
+
const location = source.path ? `path=${source.path}` : `url=${source.url || 'n/a'}`
|
|
2393
|
+
lines.push(` -${idText} ${location}`)
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
lines.push(remediation)
|
|
2397
|
+
lines.push('No source startup or registration was performed for this conflicting selection.')
|
|
2398
|
+
return lines.join('\n')
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2248
2401
|
function writeQuickstartPortFallbackInfo(output, ui, sources = []) {
|
|
2249
2402
|
for (const source of sources) {
|
|
2250
2403
|
const requestedPort = sourceRequestedPort(source)
|
|
@@ -2521,21 +2674,24 @@ function sourceMcpStreamUrl(sourceUrl) {
|
|
|
2521
2674
|
|
|
2522
2675
|
function sourceEndpointUrl(sourceUrl, endpointPath) {
|
|
2523
2676
|
const parsed = new URL(sourceUrl)
|
|
2524
|
-
const basePath = parsed.pathname
|
|
2677
|
+
const basePath = trimTrailingSlash(parsed.pathname)
|
|
2525
2678
|
parsed.pathname = `${basePath}${endpointPath}`
|
|
2526
2679
|
parsed.search = ''
|
|
2527
2680
|
parsed.hash = ''
|
|
2528
2681
|
return trimTrailingSlash(parsed.toString())
|
|
2529
2682
|
}
|
|
2530
2683
|
|
|
2531
|
-
function formatBridgeHandoff(bridgeUrl) {
|
|
2684
|
+
function formatBridgeHandoff(bridgeUrl, { smokeMode = '' } = {}) {
|
|
2532
2685
|
const baseUrl = trimTrailingSlash(bridgeUrl)
|
|
2686
|
+
const bodyLines = smokeMode === BRIDGE_MODE_EVIDENCE_ONLY
|
|
2687
|
+
? ['Bridge endpoints responded; evidence-only smoke did not check delegated-runtime reachability.']
|
|
2688
|
+
: ['Ready bridge endpoints']
|
|
2533
2689
|
return formatSummaryCard('Bridge handoff', [
|
|
2534
2690
|
['MCP JSON-RPC', `POST ${sourceEndpointUrl(baseUrl, '/mcp')}`],
|
|
2535
2691
|
['A2A answer', `POST ${sourceEndpointUrl(baseUrl, '/message:send')}`],
|
|
2536
2692
|
['Settings', sourceEndpointUrl(baseUrl, '/settings')],
|
|
2537
2693
|
['Base URL', baseUrl],
|
|
2538
|
-
], { bodyLines
|
|
2694
|
+
], { bodyLines }) + [
|
|
2539
2695
|
'Use the endpoint your agent or script supports; exact client configuration syntax varies by client.',
|
|
2540
2696
|
].join('\n') + '\n'
|
|
2541
2697
|
}
|
|
@@ -4440,25 +4596,337 @@ export async function doctor({ bridgeUrl = DEFAULT_BRIDGE_URL, serveInvocation =
|
|
|
4440
4596
|
return { checks }
|
|
4441
4597
|
}
|
|
4442
4598
|
|
|
4599
|
+
export async function statusSources({ bridgeUrl = DEFAULT_BRIDGE_URL, configPath = defaultConfigPath(), sourceHealthTimeoutMs = DEFAULT_STATUS_SOURCE_HEALTH_TIMEOUT_MS, bridgeTimeoutMs = DEFAULT_STATUS_BRIDGE_TIMEOUT_MS } = {}) {
|
|
4600
|
+
const resolvedConfigPath = resolve(configPath)
|
|
4601
|
+
const warnings = []
|
|
4602
|
+
let localSources = []
|
|
4603
|
+
let configError = ''
|
|
4604
|
+
const configExists = safeIsFile(resolvedConfigPath)
|
|
4605
|
+
if (configExists) {
|
|
4606
|
+
try {
|
|
4607
|
+
localSources = readSourceConfig(resolvedConfigPath)
|
|
4608
|
+
} catch (error) {
|
|
4609
|
+
configError = error.message
|
|
4610
|
+
warnings.push(statusWarning('source_config_unreadable', `Could not read source config: ${error.message}`))
|
|
4611
|
+
}
|
|
4612
|
+
} else {
|
|
4613
|
+
warnings.push(statusWarning('source_config_missing', `Source config not found: ${resolvedConfigPath}`))
|
|
4614
|
+
}
|
|
4615
|
+
|
|
4616
|
+
const bridge = await readBridgeStatus(bridgeUrl, { timeoutMs: bridgeTimeoutMs })
|
|
4617
|
+
if (!bridge.health.ok) {
|
|
4618
|
+
warnings.push(statusWarning('bridge_unreachable', `Bridge is not reachable at ${bridge.url}: ${bridge.health.error}`))
|
|
4619
|
+
} else if (!bridge.registry.ok) {
|
|
4620
|
+
warnings.push(statusWarning('bridge_registry_unreadable', `Bridge registry could not be read at ${bridge.url}: ${bridge.registry.error}`))
|
|
4621
|
+
}
|
|
4622
|
+
|
|
4623
|
+
const registrySources = bridge.registry.sources || []
|
|
4624
|
+
const sources = await Promise.all(localSources.map(async (source) => {
|
|
4625
|
+
const liveHealth = await probeStatusSourceHealth(source, { timeoutMs: sourceHealthTimeoutMs })
|
|
4626
|
+
const registration = reconcileSourceRegistration(source, registrySources, bridge)
|
|
4627
|
+
return statusSourceDescriptor(source, {
|
|
4628
|
+
health: liveHealth,
|
|
4629
|
+
registration,
|
|
4630
|
+
})
|
|
4631
|
+
}))
|
|
4632
|
+
|
|
4633
|
+
warnings.push(...statusConflictWarnings(sources))
|
|
4634
|
+
warnings.push(...sources
|
|
4635
|
+
.filter((source) => source.registration.state === 'id-conflict')
|
|
4636
|
+
.map((source) => statusWarning(
|
|
4637
|
+
'source_id_registered_elsewhere',
|
|
4638
|
+
`Source ${source.id} is started at ${source.url} but the bridge registry points that id at ${source.registration.registeredUrl}.`,
|
|
4639
|
+
[source.id],
|
|
4640
|
+
)))
|
|
4641
|
+
|
|
4642
|
+
return {
|
|
4643
|
+
command: 'status',
|
|
4644
|
+
generatedAt: new Date().toISOString(),
|
|
4645
|
+
configPath: resolvedConfigPath,
|
|
4646
|
+
configExists,
|
|
4647
|
+
...(configError ? { configError } : {}),
|
|
4648
|
+
bridgeUrl: trimTrailingSlash(bridgeUrl),
|
|
4649
|
+
bridge,
|
|
4650
|
+
sources,
|
|
4651
|
+
startedCount: sources.length,
|
|
4652
|
+
healthyCount: sources.filter((source) => source.health.ok).length,
|
|
4653
|
+
registeredCount: sources.filter((source) => source.registered === true).length,
|
|
4654
|
+
unregisteredCount: sources.filter((source) => source.registered === false).length,
|
|
4655
|
+
unknownRegistrationCount: sources.filter((source) => source.registered === null).length,
|
|
4656
|
+
warningCount: warnings.length,
|
|
4657
|
+
warnings,
|
|
4658
|
+
}
|
|
4659
|
+
}
|
|
4660
|
+
|
|
4661
|
+
async function readBridgeStatus(bridgeUrl, { timeoutMs = DEFAULT_STATUS_BRIDGE_TIMEOUT_MS } = {}) {
|
|
4662
|
+
const url = trimTrailingSlash(bridgeUrl)
|
|
4663
|
+
const health = await probeBridgeStatusHealth(url, { timeoutMs })
|
|
4664
|
+
if (!health.ok) {
|
|
4665
|
+
return {
|
|
4666
|
+
url,
|
|
4667
|
+
reachable: false,
|
|
4668
|
+
health,
|
|
4669
|
+
registry: { ok: false, sources: [], error: 'bridge health check failed' },
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
try {
|
|
4673
|
+
const registry = await fetchJson(new URL('/settings/sources.json', url), { timeoutMs })
|
|
4674
|
+
const sources = Array.isArray(registry.sources)
|
|
4675
|
+
? registry.sources.map(statusRegistrySourceDescriptor)
|
|
4676
|
+
: []
|
|
4677
|
+
return {
|
|
4678
|
+
url,
|
|
4679
|
+
reachable: true,
|
|
4680
|
+
health,
|
|
4681
|
+
registry: {
|
|
4682
|
+
ok: true,
|
|
4683
|
+
sources,
|
|
4684
|
+
registeredCount: sources.length,
|
|
4685
|
+
persistence: registry.persistence,
|
|
4686
|
+
},
|
|
4687
|
+
}
|
|
4688
|
+
} catch (error) {
|
|
4689
|
+
return {
|
|
4690
|
+
url,
|
|
4691
|
+
reachable: true,
|
|
4692
|
+
health,
|
|
4693
|
+
registry: {
|
|
4694
|
+
ok: false,
|
|
4695
|
+
sources: [],
|
|
4696
|
+
error: error.message,
|
|
4697
|
+
},
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4700
|
+
}
|
|
4701
|
+
|
|
4702
|
+
async function probeBridgeStatusHealth(bridgeUrl, { timeoutMs = DEFAULT_STATUS_BRIDGE_TIMEOUT_MS } = {}) {
|
|
4703
|
+
try {
|
|
4704
|
+
const health = await fetchJson(new URL('/health', bridgeUrl), { timeoutMs })
|
|
4705
|
+
return {
|
|
4706
|
+
ok: true,
|
|
4707
|
+
status: health.status || 'reachable',
|
|
4708
|
+
url: bridgeUrl,
|
|
4709
|
+
sourceRegistry: health.sourceRegistry,
|
|
4710
|
+
runtimeConnection: health.runtimeConnection,
|
|
4711
|
+
}
|
|
4712
|
+
} catch (error) {
|
|
4713
|
+
return { ok: false, error: error.message, url: bridgeUrl }
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4717
|
+
async function probeStatusSourceHealth(source, { timeoutMs = DEFAULT_STATUS_SOURCE_HEALTH_TIMEOUT_MS } = {}) {
|
|
4718
|
+
if (!source?.url) {
|
|
4719
|
+
return { ok: false, error: 'missing source URL' }
|
|
4720
|
+
}
|
|
4721
|
+
try {
|
|
4722
|
+
const health = await fetchJson(new URL('/health', source.url), { timeoutMs })
|
|
4723
|
+
return {
|
|
4724
|
+
ok: true,
|
|
4725
|
+
status: health.status || 'reachable',
|
|
4726
|
+
url: normalizeStatusUrl(source.url),
|
|
4727
|
+
}
|
|
4728
|
+
} catch (error) {
|
|
4729
|
+
return {
|
|
4730
|
+
ok: false,
|
|
4731
|
+
error: error.message,
|
|
4732
|
+
url: normalizeStatusUrl(source.url),
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4735
|
+
}
|
|
4736
|
+
|
|
4737
|
+
function reconcileSourceRegistration(source, registrySources = [], bridge = {}) {
|
|
4738
|
+
if (!bridge.reachable || !bridge.registry?.ok) {
|
|
4739
|
+
return {
|
|
4740
|
+
state: 'unknown',
|
|
4741
|
+
registered: null,
|
|
4742
|
+
reason: bridge.reachable ? 'bridge registry unavailable' : 'bridge unreachable',
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
const sourceUrl = normalizeStatusUrl(source.url)
|
|
4746
|
+
const idMatches = registrySources.filter((candidate) => String(candidate.id || '') === String(source.id || ''))
|
|
4747
|
+
const exactIdMatch = idMatches.find((candidate) => normalizeStatusUrl(candidate.url) === sourceUrl)
|
|
4748
|
+
if (exactIdMatch) {
|
|
4749
|
+
return {
|
|
4750
|
+
state: 'registered',
|
|
4751
|
+
registered: true,
|
|
4752
|
+
match: 'id+url',
|
|
4753
|
+
registeredId: exactIdMatch.id,
|
|
4754
|
+
registeredUrl: exactIdMatch.url,
|
|
4755
|
+
selected: exactIdMatch.selected,
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4758
|
+
const urlMatch = registrySources.find((candidate) => normalizeStatusUrl(candidate.url) === sourceUrl)
|
|
4759
|
+
if (urlMatch) {
|
|
4760
|
+
return {
|
|
4761
|
+
state: 'registered',
|
|
4762
|
+
registered: true,
|
|
4763
|
+
match: 'url',
|
|
4764
|
+
registeredId: urlMatch.id,
|
|
4765
|
+
registeredUrl: urlMatch.url,
|
|
4766
|
+
selected: urlMatch.selected,
|
|
4767
|
+
}
|
|
4768
|
+
}
|
|
4769
|
+
if (idMatches.length) {
|
|
4770
|
+
const registered = idMatches[0]
|
|
4771
|
+
return {
|
|
4772
|
+
state: 'id-conflict',
|
|
4773
|
+
registered: false,
|
|
4774
|
+
match: 'id',
|
|
4775
|
+
registeredId: registered.id,
|
|
4776
|
+
registeredUrl: registered.url,
|
|
4777
|
+
selected: registered.selected,
|
|
4778
|
+
reason: 'same source id is registered with a different URL',
|
|
4779
|
+
}
|
|
4780
|
+
}
|
|
4781
|
+
return {
|
|
4782
|
+
state: 'not-registered',
|
|
4783
|
+
registered: false,
|
|
4784
|
+
reason: 'not found in bridge registry',
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
|
|
4788
|
+
function statusSourceDescriptor(source = {}, { health, registration } = {}) {
|
|
4789
|
+
const processId = source.processId || source.pid || null
|
|
4790
|
+
const manifest = source.manifest || {}
|
|
4791
|
+
return {
|
|
4792
|
+
id: source.id || manifest.source_id || '',
|
|
4793
|
+
name: source.name || source.title || source.id || '',
|
|
4794
|
+
title: source.title || source.name || source.id || '',
|
|
4795
|
+
url: normalizeStatusUrl(source.url || ''),
|
|
4796
|
+
path: source.path || source.root || '',
|
|
4797
|
+
root: source.root || source.path || '',
|
|
4798
|
+
protocol: source.protocol || 'llmwiki-http',
|
|
4799
|
+
status: source.status || '',
|
|
4800
|
+
selected: source.selected !== false,
|
|
4801
|
+
processId,
|
|
4802
|
+
processAlive: processId ? processIsAlive(processId) : null,
|
|
4803
|
+
runnerProcessId: source.runnerProcessId,
|
|
4804
|
+
manifest,
|
|
4805
|
+
bundle_id: manifest.bundle_id,
|
|
4806
|
+
bundleId: manifest.bundleId || manifest.bundle_id,
|
|
4807
|
+
page_count: manifest.page_count,
|
|
4808
|
+
pageCount: manifest.pageCount ?? manifest.page_count,
|
|
4809
|
+
approved_page_count: manifest.approved_page_count,
|
|
4810
|
+
approvedPageCount: manifest.approvedPageCount ?? manifest.approved_page_count,
|
|
4811
|
+
health,
|
|
4812
|
+
registered: registration?.registered ?? null,
|
|
4813
|
+
registration: registration || { state: 'unknown', registered: null },
|
|
4814
|
+
logs: source.logs,
|
|
4815
|
+
}
|
|
4816
|
+
}
|
|
4817
|
+
|
|
4818
|
+
function statusRegistrySourceDescriptor(source = {}) {
|
|
4819
|
+
return {
|
|
4820
|
+
id: source.id || '',
|
|
4821
|
+
name: source.name || source.title || source.id || '',
|
|
4822
|
+
title: source.title || source.name || source.id || '',
|
|
4823
|
+
url: normalizeStatusUrl(source.url || ''),
|
|
4824
|
+
protocol: source.protocol || 'llmwiki-http',
|
|
4825
|
+
status: source.status || '',
|
|
4826
|
+
selected: source.selected !== false,
|
|
4827
|
+
root: source.root || source.path || '',
|
|
4828
|
+
bundleId: source.bundleId || source.bundle_id,
|
|
4829
|
+
pageCount: source.pageCount ?? source.page_count,
|
|
4830
|
+
approvedPageCount: source.approvedPageCount ?? source.approved_page_count,
|
|
4831
|
+
}
|
|
4832
|
+
}
|
|
4833
|
+
|
|
4834
|
+
function statusConflictWarnings(sources = []) {
|
|
4835
|
+
return [
|
|
4836
|
+
...selectedSourceIdConflicts(sources).map((conflict) => statusWarning(
|
|
4837
|
+
conflict.code,
|
|
4838
|
+
conflict.detail,
|
|
4839
|
+
conflict.sources.map((source) => source.id).filter(Boolean),
|
|
4840
|
+
)),
|
|
4841
|
+
...selectedSourcePathConflicts(sources).map((conflict) => statusWarning(
|
|
4842
|
+
conflict.code,
|
|
4843
|
+
conflict.detail,
|
|
4844
|
+
conflict.sources.map((source) => source.id).filter(Boolean),
|
|
4845
|
+
)),
|
|
4846
|
+
]
|
|
4847
|
+
}
|
|
4848
|
+
|
|
4849
|
+
function statusWarning(code, message, sourceIds = []) {
|
|
4850
|
+
return {
|
|
4851
|
+
severity: 'warning',
|
|
4852
|
+
code,
|
|
4853
|
+
message,
|
|
4854
|
+
...(sourceIds.length ? { sourceIds } : {}),
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
|
|
4858
|
+
function processIsAlive(pid) {
|
|
4859
|
+
if (!pid) {
|
|
4860
|
+
return null
|
|
4861
|
+
}
|
|
4862
|
+
try {
|
|
4863
|
+
process.kill(Number(pid), 0)
|
|
4864
|
+
return true
|
|
4865
|
+
} catch {
|
|
4866
|
+
return false
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
|
|
4870
|
+
function normalizeStatusUrl(value) {
|
|
4871
|
+
const text = String(value || '').trim()
|
|
4872
|
+
if (!text) {
|
|
4873
|
+
return ''
|
|
4874
|
+
}
|
|
4875
|
+
try {
|
|
4876
|
+
return redactUrlCredentials(text)
|
|
4877
|
+
} catch {
|
|
4878
|
+
return trimTrailingSlash(text)
|
|
4879
|
+
}
|
|
4880
|
+
}
|
|
4881
|
+
|
|
4882
|
+
function redactUrlCredentials(value) {
|
|
4883
|
+
if (!value) {
|
|
4884
|
+
return ''
|
|
4885
|
+
}
|
|
4886
|
+
const parsed = new URL(value)
|
|
4887
|
+
parsed.username = ''
|
|
4888
|
+
parsed.password = ''
|
|
4889
|
+
return trimTrailingSlash(parsed.toString())
|
|
4890
|
+
}
|
|
4891
|
+
|
|
4443
4892
|
export async function startSources({ paths, host = DEFAULT_HOST, portStart = DEFAULT_PORT_START, ports = [], serveInvocation = resolveServeInvocation({}), configPath = defaultConfigPath(), logDir = defaultLogDir(), healthTimeoutMs = DEFAULT_SOURCE_HEALTH_TIMEOUT_MS, healthIntervalMs = DEFAULT_SOURCE_HEALTH_INTERVAL_MS } = {}) {
|
|
4444
4893
|
if (!paths?.length) {
|
|
4445
4894
|
throw new Error('start requires at least one --path')
|
|
4446
4895
|
}
|
|
4447
4896
|
mkdirSync(logDir, { recursive: true })
|
|
4897
|
+
const sourcePlans = []
|
|
4898
|
+
for (let index = 0; index < paths.length; index += 1) {
|
|
4899
|
+
const path = resolve(paths[index])
|
|
4900
|
+
if (!safeIsDirectory(path)) {
|
|
4901
|
+
throw new Error(`Source path is not a directory: ${path}`)
|
|
4902
|
+
}
|
|
4903
|
+
sourcePlans.push({ index, path })
|
|
4904
|
+
}
|
|
4905
|
+
assertNoSelectedSourcePathConflicts(sourcePlans, { action: 'source startup' })
|
|
4906
|
+
for (const plan of sourcePlans) {
|
|
4907
|
+
const manifest = await llmwikiServeJson(serveInvocation, ['manifest', plan.path], { timeoutMs: 30000 })
|
|
4908
|
+
const sourceId = manifest.source_id || slug(manifest.title || basename(plan.path))
|
|
4909
|
+
Object.assign(plan, {
|
|
4910
|
+
manifest,
|
|
4911
|
+
sourceId,
|
|
4912
|
+
summarizedManifest: summarizeManifest(manifest),
|
|
4913
|
+
})
|
|
4914
|
+
}
|
|
4915
|
+
assertNoValidatedSourceIdConflicts(sourcePlans.map((plan) => ({
|
|
4916
|
+
id: plan.sourceId,
|
|
4917
|
+
path: plan.path,
|
|
4918
|
+
manifest: plan.summarizedManifest,
|
|
4919
|
+
})), { action: 'source startup' })
|
|
4920
|
+
|
|
4448
4921
|
const sources = []
|
|
4449
4922
|
const startedProcesses = []
|
|
4450
4923
|
const assignedPorts = new Set()
|
|
4451
4924
|
try {
|
|
4452
|
-
for (
|
|
4453
|
-
const path =
|
|
4454
|
-
if (!safeIsDirectory(path)) {
|
|
4455
|
-
throw new Error(`Source path is not a directory: ${path}`)
|
|
4456
|
-
}
|
|
4925
|
+
for (const plan of sourcePlans) {
|
|
4926
|
+
const { index, path, manifest, sourceId, summarizedManifest } = plan
|
|
4457
4927
|
const requestedPort = ports[index] || portStart + index
|
|
4458
4928
|
const port = await nextAvailablePort(requestedPort, { host, unavailable: assignedPorts })
|
|
4459
4929
|
assignedPorts.add(port)
|
|
4460
|
-
const manifest = await llmwikiServeJson(serveInvocation, ['manifest', path], { timeoutMs: 30000 })
|
|
4461
|
-
const sourceId = manifest.source_id || slug(manifest.title || basename(path))
|
|
4462
4930
|
const out = join(logDir, `${sourceId}-${port}.out.log`)
|
|
4463
4931
|
const err = join(logDir, `${sourceId}-${port}.err.log`)
|
|
4464
4932
|
const outFd = openSync(out, 'a')
|
|
@@ -4507,7 +4975,7 @@ export async function startSources({ paths, host = DEFAULT_HOST, portStart = DEF
|
|
|
4507
4975
|
...(port !== requestedPort ? { portFallback: { requestedPort, assignedPort: port, reason: 'requested-port-occupied' } } : {}),
|
|
4508
4976
|
processId,
|
|
4509
4977
|
runnerProcessId: serverProcessId && serverProcessId !== child.pid ? child.pid : undefined,
|
|
4510
|
-
manifest:
|
|
4978
|
+
manifest: summarizedManifest,
|
|
4511
4979
|
health,
|
|
4512
4980
|
logs,
|
|
4513
4981
|
})
|
|
@@ -4821,6 +5289,11 @@ export async function registerSources({ bridgeUrl = DEFAULT_BRIDGE_URL, configPa
|
|
|
4821
5289
|
const normalized = sources.map((source, index) => normalizeBridgeSource(source, {
|
|
4822
5290
|
selected: selectedIds.size ? selectedIds.has(source.id) : (source.selected ?? (selectFirst && index === 0)),
|
|
4823
5291
|
}))
|
|
5292
|
+
assertNoValidatedSourceIdConflicts(normalized.map((source) => ({
|
|
5293
|
+
id: source.id,
|
|
5294
|
+
path: source.path,
|
|
5295
|
+
url: source.url,
|
|
5296
|
+
})), { action: 'source registration' })
|
|
4824
5297
|
|
|
4825
5298
|
const existing = replace ? [] : await readExistingBridgeSources(bridgeUrl)
|
|
4826
5299
|
const merged = applySelectedIds(mergeBridgeSources(existing, normalized), selectedIds)
|
|
@@ -4894,7 +5367,7 @@ export function mergeBridgeSources(existing, incoming) {
|
|
|
4894
5367
|
|
|
4895
5368
|
function sameBridgeSource(left, right) {
|
|
4896
5369
|
return Boolean(left.id && right.id && left.id === right.id)
|
|
4897
|
-
|| Boolean(left.url && right.url && left.url
|
|
5370
|
+
|| Boolean(left.url && right.url && trimTrailingSlash(left.url) === trimTrailingSlash(right.url))
|
|
4898
5371
|
}
|
|
4899
5372
|
|
|
4900
5373
|
function assertSafeSourceUrl(value) {
|
|
@@ -4905,7 +5378,7 @@ function assertSafeSourceUrl(value) {
|
|
|
4905
5378
|
if (parsed.username || parsed.password) {
|
|
4906
5379
|
throw new Error('Source URL must not contain credentials.')
|
|
4907
5380
|
}
|
|
4908
|
-
return parsed.toString()
|
|
5381
|
+
return trimTrailingSlash(parsed.toString())
|
|
4909
5382
|
}
|
|
4910
5383
|
|
|
4911
5384
|
export function bridgeStartPlan(options = {}) {
|
|
@@ -5246,13 +5719,12 @@ export async function probeRuntimeEndpoint({ baseUrl, profile = 'generic', timeo
|
|
|
5246
5719
|
const normalizedBaseUrl = normalizeRuntimeBaseUrl(baseUrl)
|
|
5247
5720
|
const runtimeProfile = parseRuntimeProfile(profile, 'generic')
|
|
5248
5721
|
if (runtimeProfile !== RUNTIME_SETUP_HERMES) {
|
|
5249
|
-
return {
|
|
5250
|
-
ok: true,
|
|
5251
|
-
skipped: true,
|
|
5252
|
-
profile: runtimeProfile,
|
|
5722
|
+
return probeOpenAiCompatibleRuntimeEndpoint({
|
|
5253
5723
|
baseUrl: normalizedBaseUrl,
|
|
5254
|
-
|
|
5255
|
-
|
|
5724
|
+
profile: runtimeProfile,
|
|
5725
|
+
timeoutMs,
|
|
5726
|
+
fetchRuntimeJson,
|
|
5727
|
+
})
|
|
5256
5728
|
}
|
|
5257
5729
|
const healthUrls = hermesHealthUrls(normalizedBaseUrl)
|
|
5258
5730
|
const errors = []
|
|
@@ -5283,10 +5755,43 @@ export async function probeRuntimeEndpoint({ baseUrl, profile = 'generic', timeo
|
|
|
5283
5755
|
}
|
|
5284
5756
|
}
|
|
5285
5757
|
|
|
5758
|
+
async function probeOpenAiCompatibleRuntimeEndpoint({ baseUrl, profile, timeoutMs = DEFAULT_RUNTIME_REACHABILITY_TIMEOUT_MS, fetchRuntimeJson = fetchJson } = {}) {
|
|
5759
|
+
const modelsUrl = openAiCompatibleModelsUrl(baseUrl)
|
|
5760
|
+
try {
|
|
5761
|
+
await fetchRuntimeJson(modelsUrl, { timeoutMs })
|
|
5762
|
+
return {
|
|
5763
|
+
ok: true,
|
|
5764
|
+
profile,
|
|
5765
|
+
baseUrl,
|
|
5766
|
+
url: modelsUrl,
|
|
5767
|
+
status: 'reachable',
|
|
5768
|
+
endpoint: 'models',
|
|
5769
|
+
}
|
|
5770
|
+
} catch (error) {
|
|
5771
|
+
return {
|
|
5772
|
+
ok: false,
|
|
5773
|
+
profile,
|
|
5774
|
+
baseUrl,
|
|
5775
|
+
url: modelsUrl,
|
|
5776
|
+
endpoint: 'models',
|
|
5777
|
+
error: error.message,
|
|
5778
|
+
}
|
|
5779
|
+
}
|
|
5780
|
+
}
|
|
5781
|
+
|
|
5782
|
+
function openAiCompatibleModelsUrl(baseUrl) {
|
|
5783
|
+
const parsed = new URL(baseUrl)
|
|
5784
|
+
const basePath = trimTrailingSlash(parsed.pathname)
|
|
5785
|
+
parsed.pathname = `${basePath || ''}/models`
|
|
5786
|
+
parsed.search = ''
|
|
5787
|
+
parsed.hash = ''
|
|
5788
|
+
return parsed.toString()
|
|
5789
|
+
}
|
|
5790
|
+
|
|
5286
5791
|
function hermesHealthUrls(baseUrl) {
|
|
5287
5792
|
const parsed = new URL(baseUrl)
|
|
5288
5793
|
const origin = `${parsed.protocol}//${parsed.host}`
|
|
5289
|
-
const basePath = parsed.pathname
|
|
5794
|
+
const basePath = trimTrailingSlash(parsed.pathname)
|
|
5290
5795
|
const paths = ['/health']
|
|
5291
5796
|
if (basePath && basePath !== '/') {
|
|
5292
5797
|
paths.push(`${basePath}/health`)
|
|
@@ -5418,10 +5923,15 @@ function bridgeModeOption(value, fallback) {
|
|
|
5418
5923
|
}
|
|
5419
5924
|
|
|
5420
5925
|
function trimTrailingSlash(value) {
|
|
5421
|
-
|
|
5926
|
+
const text = String(value || '')
|
|
5927
|
+
let end = text.length
|
|
5928
|
+
while (end > 0 && text.charCodeAt(end - 1) === 47) {
|
|
5929
|
+
end -= 1
|
|
5930
|
+
}
|
|
5931
|
+
return end === text.length ? text : text.slice(0, end)
|
|
5422
5932
|
}
|
|
5423
5933
|
|
|
5424
|
-
export async function selectBridgeSmokeMode({ options = {}, bridgeUrl = DEFAULT_BRIDGE_URL, env = process.env, inspectBridgeRuntime = inspectBridgeRuntimeConfiguration } = {}) {
|
|
5934
|
+
export async function selectBridgeSmokeMode({ options = {}, bridgeUrl = DEFAULT_BRIDGE_URL, env = process.env, inspectBridgeRuntime = inspectBridgeRuntimeConfiguration, probeRuntime = probeRuntimeEndpoint } = {}) {
|
|
5425
5935
|
const requestedMode = stringOption(options.mode ?? options['orchestration-mode'], '')
|
|
5426
5936
|
if (requestedMode) {
|
|
5427
5937
|
return { mode: bridgeModeOption(requestedMode, BRIDGE_MODE_EVIDENCE_ONLY), reason: 'requested with --mode' }
|
|
@@ -5433,8 +5943,18 @@ export async function selectBridgeSmokeMode({ options = {}, bridgeUrl = DEFAULT_
|
|
|
5433
5943
|
|
|
5434
5944
|
const runtimeInfo = detectLlmRuntime(options, env)
|
|
5435
5945
|
if (runtimeInfo.configured) {
|
|
5946
|
+
if (runtimeInfo.baseUrl) {
|
|
5947
|
+
const readiness = await probeRuntime({ baseUrl: runtimeInfo.baseUrl, profile: runtimeInfo.profile })
|
|
5948
|
+
if (!readiness.ok) {
|
|
5949
|
+
return {
|
|
5950
|
+
mode: BRIDGE_MODE_EVIDENCE_ONLY,
|
|
5951
|
+
reason: `explicit LLM endpoint was configured but not reachable (${readiness.error || 'runtime probe failed'})`,
|
|
5952
|
+
runtimeReachability: readiness,
|
|
5953
|
+
}
|
|
5954
|
+
}
|
|
5955
|
+
}
|
|
5436
5956
|
const reason = runtimeInfo.baseUrl
|
|
5437
|
-
? `explicit LLM endpoint configured (${runtimeInfo.baseUrl})`
|
|
5957
|
+
? `explicit LLM endpoint configured and reachable (${runtimeInfo.baseUrl})`
|
|
5438
5958
|
: `explicit runtime adapter configured (${runtimeInfo.runtimeAdapter})`
|
|
5439
5959
|
return { mode: BRIDGE_MODE_DELEGATED_RUNTIME, reason }
|
|
5440
5960
|
}
|
|
@@ -5451,30 +5971,163 @@ export async function selectBridgeSmokeMode({ options = {}, bridgeUrl = DEFAULT_
|
|
|
5451
5971
|
}
|
|
5452
5972
|
|
|
5453
5973
|
async function inspectBridgeRuntimeConfiguration(bridgeUrl) {
|
|
5454
|
-
const
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5974
|
+
const runtimeStatus = await inspectBridgeRuntimeForSmoke(bridgeUrl)
|
|
5975
|
+
if (runtimeStatus.configured && runtimeStatus.reachable) {
|
|
5976
|
+
return { configured: true, reason: runtimeStatus.reason, runtimeStatus }
|
|
5977
|
+
}
|
|
5978
|
+
return {
|
|
5979
|
+
configured: false,
|
|
5980
|
+
reason: runtimeStatus.reason || 'no reachable LLM endpoint detected in bridge settings',
|
|
5981
|
+
runtimeStatus,
|
|
5462
5982
|
}
|
|
5463
|
-
return { configured: false, reason: 'no explicit LLM endpoint detected in bridge settings' }
|
|
5464
5983
|
}
|
|
5465
5984
|
|
|
5466
|
-
|
|
5467
|
-
const
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5985
|
+
async function inspectBridgeRuntimeForSmoke(bridgeUrl, { runtime = null, probeRuntime = probeRuntimeEndpoint, timeoutMs = DEFAULT_RUNTIME_REACHABILITY_TIMEOUT_MS } = {}) {
|
|
5986
|
+
const runtimeInfo = runtime?.configured
|
|
5987
|
+
? runtime
|
|
5988
|
+
: await bridgeRuntimeInfoFromSettings(bridgeUrl)
|
|
5989
|
+
if (!runtimeInfo.configured) {
|
|
5990
|
+
return {
|
|
5991
|
+
configured: false,
|
|
5992
|
+
reachable: false,
|
|
5993
|
+
reason: runtimeInfo.reason || 'no runtime endpoint is configured',
|
|
5994
|
+
runtime: runtimeInfo,
|
|
5995
|
+
}
|
|
5996
|
+
}
|
|
5997
|
+
if (runtimeInfo.runtimeAdapter === RUNTIME_ADAPTER_DEEPAGENTS_ACP && !runtimeInfo.baseUrl) {
|
|
5998
|
+
return {
|
|
5999
|
+
configured: true,
|
|
6000
|
+
reachable: true,
|
|
6001
|
+
checked: false,
|
|
6002
|
+
reason: 'explicit runtime adapter configured; OpenAI-compatible endpoint probe is not applicable',
|
|
6003
|
+
runtime: runtimeInfo,
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
if (!runtimeInfo.baseUrl) {
|
|
6007
|
+
return {
|
|
6008
|
+
configured: false,
|
|
6009
|
+
reachable: false,
|
|
6010
|
+
reason: 'runtime is configured without a base URL',
|
|
6011
|
+
runtime: runtimeInfo,
|
|
6012
|
+
}
|
|
6013
|
+
}
|
|
6014
|
+
const reachability = await probeRuntime({
|
|
6015
|
+
baseUrl: runtimeInfo.baseUrl,
|
|
6016
|
+
profile: runtimeInfo.profile || 'generic',
|
|
6017
|
+
timeoutMs,
|
|
5473
6018
|
})
|
|
6019
|
+
if (!reachability.ok) {
|
|
6020
|
+
return {
|
|
6021
|
+
configured: true,
|
|
6022
|
+
reachable: false,
|
|
6023
|
+
reason: `runtime endpoint is configured but not reachable (${reachability.error || 'probe failed'})`,
|
|
6024
|
+
runtime: runtimeInfo,
|
|
6025
|
+
reachability,
|
|
6026
|
+
}
|
|
6027
|
+
}
|
|
6028
|
+
return {
|
|
6029
|
+
configured: true,
|
|
6030
|
+
reachable: true,
|
|
6031
|
+
checked: true,
|
|
6032
|
+
reason: `runtime endpoint is configured and reachable (${runtimeInfo.baseUrl})`,
|
|
6033
|
+
runtime: runtimeInfo,
|
|
6034
|
+
reachability,
|
|
6035
|
+
}
|
|
6036
|
+
}
|
|
6037
|
+
|
|
6038
|
+
async function bridgeRuntimeInfoFromSettings(bridgeUrl) {
|
|
6039
|
+
try {
|
|
6040
|
+
const settings = await fetchJson(new URL('/settings.json', bridgeUrl), { timeoutMs: DEFAULT_STATUS_BRIDGE_TIMEOUT_MS })
|
|
6041
|
+
const connection = settings.runtimeConnection || {}
|
|
6042
|
+
const runtime = settings.runtime || {}
|
|
6043
|
+
const runtimeAdapter = parseRuntimeAdapterOption(runtime.adapter || settings.runtimeAdapter) || ''
|
|
6044
|
+
const profile = parseRuntimeProfile(runtime.profile || settings.runtimeProfile || connection.runtimeProfile || 'generic', 'generic')
|
|
6045
|
+
const model = String(connection.model || settings.model || '').trim()
|
|
6046
|
+
|| (connection.modelConfigured ? 'configured-model' : '')
|
|
6047
|
+
const rawBaseUrl = String(connection.baseUrl || settings.baseUrl || '').trim()
|
|
6048
|
+
const baseUrl = runtimeBaseUrlFromSettings(rawBaseUrl)
|
|
6049
|
+
const configured = Boolean(
|
|
6050
|
+
runtimeAdapter === RUNTIME_ADAPTER_DEEPAGENTS_ACP
|
|
6051
|
+
|| (baseUrl && baseUrl !== 'none' && connection.modelConfigured),
|
|
6052
|
+
)
|
|
6053
|
+
return {
|
|
6054
|
+
configured,
|
|
6055
|
+
baseUrl: baseUrl === 'none' ? '' : baseUrl,
|
|
6056
|
+
model,
|
|
6057
|
+
profile,
|
|
6058
|
+
runtimeAdapter,
|
|
6059
|
+
modelConfigured: Boolean(connection.modelConfigured),
|
|
6060
|
+
baseUrlSource: connection.baseUrlSource || '',
|
|
6061
|
+
reason: configured ? 'runtime settings found on bridge' : 'no model runtime configured in bridge settings',
|
|
6062
|
+
}
|
|
6063
|
+
} catch (error) {
|
|
6064
|
+
return {
|
|
6065
|
+
configured: false,
|
|
6066
|
+
baseUrl: '',
|
|
6067
|
+
model: '',
|
|
6068
|
+
profile: 'generic',
|
|
6069
|
+
runtimeAdapter: '',
|
|
6070
|
+
reason: `could not inspect bridge runtime settings: ${error.message}`,
|
|
6071
|
+
error: error.message,
|
|
6072
|
+
}
|
|
6073
|
+
}
|
|
6074
|
+
}
|
|
6075
|
+
|
|
6076
|
+
function runtimeBaseUrlFromSettings(value) {
|
|
6077
|
+
if (!value || value === 'none') {
|
|
6078
|
+
return ''
|
|
6079
|
+
}
|
|
6080
|
+
try {
|
|
6081
|
+
return normalizeRuntimeBaseUrl(value)
|
|
6082
|
+
} catch {
|
|
6083
|
+
return ''
|
|
6084
|
+
}
|
|
6085
|
+
}
|
|
6086
|
+
|
|
6087
|
+
function formatDelegatedRuntimePreflightError(bridgeUrl, runtimeStatus = {}) {
|
|
6088
|
+
const settingsUrl = sourceEndpointUrl(trimTrailingSlash(bridgeUrl), '/settings')
|
|
6089
|
+
const runtime = runtimeStatus.runtime || {}
|
|
6090
|
+
const target = runtime.baseUrl ? ` Runtime endpoint: ${runtime.baseUrl}.` : ''
|
|
6091
|
+
return [
|
|
6092
|
+
`Delegated-runtime smoke requires a reachable model runtime before it can be reported ready.${target}`,
|
|
6093
|
+
runtimeStatus.reason || 'Runtime reachability could not be verified.',
|
|
6094
|
+
`Configure the bridge runtime at ${settingsUrl}, start the endpoint, or rerun smoke with --mode evidence-only to test sources only.`,
|
|
6095
|
+
].join(' ')
|
|
6096
|
+
}
|
|
6097
|
+
|
|
6098
|
+
export async function smokeBridge({ bridgeUrl = DEFAULT_BRIDGE_URL, query, mode = BRIDGE_MODE_EVIDENCE_ONLY, runtime = null, preflightRuntime = true, probeRuntime = probeRuntimeEndpoint } = {}) {
|
|
6099
|
+
const smokeMode = bridgeModeOption(mode, BRIDGE_MODE_EVIDENCE_ONLY)
|
|
6100
|
+
const warnings = []
|
|
6101
|
+
let runtimeReachability = null
|
|
6102
|
+
if (smokeMode === BRIDGE_MODE_EVIDENCE_ONLY) {
|
|
6103
|
+
warnings.push('Evidence-only smoke did not check delegated-runtime reachability.')
|
|
6104
|
+
} else if (preflightRuntime) {
|
|
6105
|
+
runtimeReachability = await inspectBridgeRuntimeForSmoke(bridgeUrl, { runtime, probeRuntime })
|
|
6106
|
+
if (!runtimeReachability.configured || !runtimeReachability.reachable) {
|
|
6107
|
+
throw new Error(formatDelegatedRuntimePreflightError(bridgeUrl, runtimeReachability))
|
|
6108
|
+
}
|
|
6109
|
+
}
|
|
6110
|
+
let response
|
|
6111
|
+
try {
|
|
6112
|
+
response = await fetchJson(new URL('/message:send', bridgeUrl), {
|
|
6113
|
+
method: 'POST',
|
|
6114
|
+
body: JSON.stringify({ data: { query, mode: smokeMode } }),
|
|
6115
|
+
headers: { 'content-type': 'application/json' },
|
|
6116
|
+
timeoutMs: 30000,
|
|
6117
|
+
})
|
|
6118
|
+
} catch (error) {
|
|
6119
|
+
if (smokeMode !== BRIDGE_MODE_EVIDENCE_ONLY) {
|
|
6120
|
+
throw new Error(`${error.message} Configure the model runtime at ${sourceEndpointUrl(trimTrailingSlash(bridgeUrl), '/settings')}, verify it is reachable, or rerun with --mode evidence-only to test sources only.`)
|
|
6121
|
+
}
|
|
6122
|
+
throw error
|
|
6123
|
+
}
|
|
5474
6124
|
return {
|
|
5475
6125
|
bridgeUrl,
|
|
5476
6126
|
query,
|
|
5477
6127
|
mode: smokeMode,
|
|
6128
|
+
runtimeChecked: smokeMode !== BRIDGE_MODE_EVIDENCE_ONLY && Boolean(preflightRuntime),
|
|
6129
|
+
...(runtimeReachability ? { runtimeReachability } : {}),
|
|
6130
|
+
...(warnings.length ? { warnings } : {}),
|
|
5478
6131
|
status: response.status,
|
|
5479
6132
|
text: response.status?.message?.parts?.find((part) => part.kind === 'text')?.text || '',
|
|
5480
6133
|
}
|
|
@@ -5677,9 +6330,89 @@ function writeResult(result, options, io) {
|
|
|
5677
6330
|
io.stdout.write(formatCandidates(result.candidates))
|
|
5678
6331
|
return
|
|
5679
6332
|
}
|
|
6333
|
+
if (result?.command === 'status') {
|
|
6334
|
+
io.stdout.write(formatStatusResult(result))
|
|
6335
|
+
return
|
|
6336
|
+
}
|
|
5680
6337
|
io.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
|
|
5681
6338
|
}
|
|
5682
6339
|
|
|
6340
|
+
function formatStatusResult(result = {}) {
|
|
6341
|
+
const lines = []
|
|
6342
|
+
const bridgeState = result.bridge?.health?.ok
|
|
6343
|
+
? `reachable (${result.bridge.health.status || 'ok'})`
|
|
6344
|
+
: `unreachable (${result.bridge?.health?.error || 'not checked'})`
|
|
6345
|
+
const registryState = result.bridge?.registry?.ok
|
|
6346
|
+
? `${result.bridge.registry.registeredCount || 0} registered`
|
|
6347
|
+
: `unavailable (${result.bridge?.registry?.error || 'not checked'})`
|
|
6348
|
+
lines.push(`Source config: ${result.configExists ? result.configPath : `${result.configPath} (missing)`}`)
|
|
6349
|
+
lines.push(`Bridge: ${result.bridgeUrl} - ${bridgeState}; registry ${registryState}`)
|
|
6350
|
+
lines.push(`Sources: ${result.startedCount || 0} local, ${result.healthyCount || 0} healthy, ${result.registeredCount || 0} registered`)
|
|
6351
|
+
if (!result.sources?.length) {
|
|
6352
|
+
lines.push('No local started sources found.')
|
|
6353
|
+
} else {
|
|
6354
|
+
lines.push(formatStatusSourceTable(result.sources))
|
|
6355
|
+
}
|
|
6356
|
+
if (result.warnings?.length) {
|
|
6357
|
+
lines.push('')
|
|
6358
|
+
lines.push('Warnings:')
|
|
6359
|
+
for (const warning of result.warnings) {
|
|
6360
|
+
lines.push(`- ${warning.code}: ${warning.message}`)
|
|
6361
|
+
}
|
|
6362
|
+
}
|
|
6363
|
+
lines.push('')
|
|
6364
|
+
lines.push(`Note: ${pathRedactionNoticeText()}`)
|
|
6365
|
+
return `${lines.join('\n')}\n`
|
|
6366
|
+
}
|
|
6367
|
+
|
|
6368
|
+
function formatStatusSourceTable(sources = []) {
|
|
6369
|
+
const rows = [
|
|
6370
|
+
['ID', 'URL', 'ROOT', 'HEALTH', 'REGISTERED', 'PID'],
|
|
6371
|
+
...sources.map((source) => [
|
|
6372
|
+
source.id || '',
|
|
6373
|
+
source.url || '',
|
|
6374
|
+
source.root || source.path || '',
|
|
6375
|
+
formatStatusHealth(source),
|
|
6376
|
+
formatStatusRegistration(source),
|
|
6377
|
+
formatStatusPid(source),
|
|
6378
|
+
]),
|
|
6379
|
+
]
|
|
6380
|
+
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => String(row[column] || '').length)))
|
|
6381
|
+
return rows
|
|
6382
|
+
.map((row) => row.map((cell, column) => String(cell || '').padEnd(widths[column])).join(' ').trimEnd())
|
|
6383
|
+
.join('\n')
|
|
6384
|
+
}
|
|
6385
|
+
|
|
6386
|
+
function formatStatusHealth(source = {}) {
|
|
6387
|
+
if (source.health?.ok) {
|
|
6388
|
+
return source.health.status || 'ok'
|
|
6389
|
+
}
|
|
6390
|
+
return source.health?.error ? 'fail' : 'unknown'
|
|
6391
|
+
}
|
|
6392
|
+
|
|
6393
|
+
function formatStatusRegistration(source = {}) {
|
|
6394
|
+
if (source.registration?.state === 'registered') {
|
|
6395
|
+
return source.registration.selected === false ? 'yes (unselected)' : 'yes'
|
|
6396
|
+
}
|
|
6397
|
+
if (source.registration?.state === 'id-conflict') {
|
|
6398
|
+
return `no (id at ${source.registration.registeredUrl || 'other URL'})`
|
|
6399
|
+
}
|
|
6400
|
+
if (source.registration?.state === 'not-registered') {
|
|
6401
|
+
return 'no'
|
|
6402
|
+
}
|
|
6403
|
+
return 'unknown'
|
|
6404
|
+
}
|
|
6405
|
+
|
|
6406
|
+
function formatStatusPid(source = {}) {
|
|
6407
|
+
if (!source.processId) {
|
|
6408
|
+
return ''
|
|
6409
|
+
}
|
|
6410
|
+
if (source.processAlive === null) {
|
|
6411
|
+
return String(source.processId)
|
|
6412
|
+
}
|
|
6413
|
+
return `${source.processId} ${source.processAlive ? 'alive' : 'stale'}`
|
|
6414
|
+
}
|
|
6415
|
+
|
|
5683
6416
|
function formatCandidates(candidates) {
|
|
5684
6417
|
if (!candidates.length) {
|
|
5685
6418
|
return 'No LLMWiki candidates found.\n'
|
|
@@ -5708,7 +6441,8 @@ Usage:
|
|
|
5708
6441
|
llmwiki-bridge-start discover [--home|--workspace|--cwd|--path DIR] [--validate] [--min-score 30] [--serve-command CMD] [--json]
|
|
5709
6442
|
llmwiki-bridge-start start --path DIR [--port 11001] [--serve-command CMD]
|
|
5710
6443
|
llmwiki-bridge-start register [--bridge URL] [--config FILE] [--replace]
|
|
5711
|
-
llmwiki-bridge-start
|
|
6444
|
+
llmwiki-bridge-start status|ls [--bridge URL] [--config FILE] [--json]
|
|
6445
|
+
llmwiki-bridge-start smoke [--bridge URL] [--query TEXT] [--mode evidence-only|delegated-runtime|hybrid] [--llm-endpoint URL] [--runtime-profile PROFILE]
|
|
5712
6446
|
llmwiki-bridge-start doctor [--bridge URL]
|
|
5713
6447
|
|
|
5714
6448
|
Commands:
|
|
@@ -5716,7 +6450,9 @@ Commands:
|
|
|
5716
6450
|
discover Find likely LLMWiki Markdown, Native, Obsidian, Logseq, Dendron, Foam, or Quartz roots.
|
|
5717
6451
|
start Start llmwiki-serve for explicit source paths and write a source config.
|
|
5718
6452
|
register Upsert started or explicit sources in llmwiki-agent-bridge settings.
|
|
5719
|
-
|
|
6453
|
+
status List local started sources with live health and bridge registration state.
|
|
6454
|
+
ls Alias for status.
|
|
6455
|
+
smoke Run a small bridge query; defaults to evidence-only and says when delegated runtime was not checked.
|
|
5720
6456
|
doctor Check local tool and bridge readiness.
|
|
5721
6457
|
|
|
5722
6458
|
Quickstart shows recommended LLMWiki source folders first; use --include-additional for advanced/lower-priority app vaults, examples, demos, and starter/e2e sources.
|
|
@@ -5728,7 +6464,7 @@ Bridge setup is optional. Started source URLs can be used directly without llmwi
|
|
|
5728
6464
|
After bridge setup approval, quickstart asks for runtime setup: skip/evidence-only, Hermes, or DeepAgents.
|
|
5729
6465
|
Use --runtime-adapter deepagents-acp only when explicitly opting into the DeepAgents ACP bridge adapter; DeepAgents without that flag keeps the existing endpoint-based compatibility path.
|
|
5730
6466
|
If a selected runtime CLI is missing, interactive quickstart may offer an official installer after explicit approval; --yes automation still requires --install-runtime.
|
|
5731
|
-
Bridge smoke defaults to evidence-only unless --mode or quickstart runtime setup/detection selects another mode.
|
|
6467
|
+
Bridge smoke defaults to evidence-only unless --mode or quickstart runtime setup/detection selects another mode. Delegated-runtime smoke verifies runtime reachability first; generic OpenAI-compatible endpoints use /models and Hermes uses /health or /v1/health.
|
|
5732
6468
|
Use --serve-command/--serve-arg/--serve-cwd when llmwiki-serve is not on PATH.
|
|
5733
6469
|
`
|
|
5734
6470
|
}
|