triflux 10.19.0 → 10.20.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.
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
3
+ "name": "triflux",
4
+ "description": "CLI-first multi-model orchestrator — Codex/Gemini/Claude routing with DAG execution, auto-triage, and cost optimization",
5
+ "owner": {
6
+ "name": "tellang"
7
+ },
8
+ "plugins": [
9
+ {
10
+ "name": "triflux",
11
+ "description": "Tri-CLI orchestrator for Claude Code. Routes tasks across Claude + Codex + Gemini with consensus intelligence, natural language routing, 42 skills, and cross-model review.",
12
+ "version": "10.20.1",
13
+ "author": {
14
+ "name": "tellang"
15
+ },
16
+ "source": {
17
+ "source": "npm",
18
+ "package": "triflux"
19
+ },
20
+ "category": "productivity",
21
+ "homepage": "https://github.com/tellang/triflux",
22
+ "tags": [
23
+ "multi-model",
24
+ "codex",
25
+ "gemini",
26
+ "cli-routing",
27
+ "orchestration",
28
+ "cost-optimization",
29
+ "dag-execution"
30
+ ]
31
+ }
32
+ ],
33
+ "version": "10.20.1"
34
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "triflux",
3
+ "version": "10.20.1",
4
+ "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
+ "author": {
6
+ "name": "tellang"
7
+ },
8
+ "repository": "https://github.com/tellang/triflux",
9
+ "homepage": "https://github.com/tellang/triflux",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "claude-code",
13
+ "plugin",
14
+ "codex",
15
+ "gemini",
16
+ "cli-routing",
17
+ "orchestration",
18
+ "multi-model"
19
+ ],
20
+ "skills": "./skills/",
21
+ "hooks": "./hooks/hooks.json"
22
+ }
package/bin/triflux.mjs CHANGED
@@ -77,6 +77,7 @@ import {
77
77
  getVersion,
78
78
  getWindowsHubAutostartStatus,
79
79
  hasProfileSection,
80
+ isSetupUserStateFile,
80
81
  LEGACY_CODEX_MODELS,
81
82
  REQUIRED_CODEX_PROFILES,
82
83
  replaceProfileSection,
@@ -1206,6 +1207,7 @@ function cmdSetup(options = {}) {
1206
1207
  if (existsSync(refSrc)) {
1207
1208
  mkdirSync(refDst, { recursive: true });
1208
1209
  for (const refFile of readdirSync(refSrc)) {
1210
+ if (isSetupUserStateFile(refFile)) continue;
1209
1211
  const rSrc = join(refSrc, refFile);
1210
1212
  const rDst = join(refDst, refFile);
1211
1213
  if (statSync(rSrc).isFile()) {
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "mcp-registry-schema",
3
+ "version": 1,
4
+ "description": "MCP 서버 중앙 레지스트리 — 진실의 원천",
5
+ "defaults": {
6
+ "transport": "hub-url",
7
+ "hub_base": "http://127.0.0.1:27888"
8
+ },
9
+ "policy_notes": {
10
+ "transport": "Server transport accepts \"hub-url\" for the existing triflux Hub URL flow or \"http\" for direct Streamable HTTP MCP endpoints. Direct stdio registration via command/args is intentionally unsupported.",
11
+ "headers": "Optional headers are allowed only for HTTP-compatible transports. Each header value must be a descriptor: {\"value\":\"literal\"} for non-secret static values, {\"env\":\"ENV_VAR_NAME\"} for secrets resolved at sync/runtime, or {\"env\":\"ENV_VAR_NAME\",\"prefix\":\"Bearer \"} for common authorization formats.",
12
+ "secret_safety": "Resolved secret values must not be written back to this registry file. Missing env vars warn during sync and do not emit empty secret headers.",
13
+ "sync_denylist": "Array of client:server strings skipped by proactive registry sync, for example gemini:tfx-hub."
14
+ },
15
+ "servers": {
16
+ "tfx-hub": {
17
+ "transport": "hub-url",
18
+ "url": "http://127.0.0.1:27888/mcp",
19
+ "safe": true,
20
+ "targets": ["claude", "gemini", "codex"],
21
+ "description": "triflux Hub MCP 서버"
22
+ },
23
+ "context7": {
24
+ "transport": "http",
25
+ "url": "https://mcp.context7.com/mcp",
26
+ "safe": true,
27
+ "targets": ["claude", "gemini", "codex"],
28
+ "description": "Upstash Context7 — 라이브러리 문서/코드 컨텍스트 (HTTP MCP, API key 불필요)"
29
+ }
30
+ },
31
+ "policies": {
32
+ "stdio_action": "replace-with-hub",
33
+ "unknown_server_action": "warn",
34
+ "sync_denylist": [],
35
+ "watched_paths": [
36
+ "~/.gemini/settings.json",
37
+ "~/.codex/config.toml",
38
+ "~/.claude/settings.json",
39
+ "~/.claude/settings.local.json",
40
+ ".claude/mcp.json",
41
+ ".mcp.json"
42
+ ]
43
+ }
44
+ }
@@ -264,6 +264,19 @@
264
264
  "blocking": false,
265
265
  "description": "서브에이전트 결과 품질 체크"
266
266
  }
267
+ ],
268
+ "PreCompact": [
269
+ {
270
+ "id": "tfx-pre-compact-snapshot",
271
+ "source": "triflux",
272
+ "matcher": "*",
273
+ "command": "node \"${PLUGIN_ROOT}/hooks/pre-compact-snapshot.mjs\"",
274
+ "priority": 0,
275
+ "enabled": true,
276
+ "timeout": 3,
277
+ "blocking": false,
278
+ "description": "compact 직전 repo-local 진행 상태 스냅샷 주입 (#245)"
279
+ }
267
280
  ]
268
281
  }
269
282
  }
package/hooks/hooks.json CHANGED
@@ -84,6 +84,18 @@
84
84
  }
85
85
  ]
86
86
  }
87
+ ],
88
+ "PreCompact": [
89
+ {
90
+ "matcher": "*",
91
+ "hooks": [
92
+ {
93
+ "type": "command",
94
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/hook-orchestrator.mjs\"",
95
+ "timeout": 5
96
+ }
97
+ ]
98
+ }
87
99
  ]
88
100
  }
89
101
  }
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ // hooks/pre-compact-snapshot.mjs — PreCompact hook
3
+ //
4
+ // Compaction 직전에 짧은 상태 스냅샷을 주입한다. 민감 데이터는 읽지 않고,
5
+ // repo-local 상태와 git 요약만 3KB 이하로 제한한다.
6
+
7
+ import { execFileSync } from "node:child_process";
8
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+
12
+ const MAX_CONTEXT_BYTES = 3000;
13
+ const STALE_RUN_MS = 30 * 60 * 1000;
14
+ const PROJECT_ROOT = process.env.CLAUDE_CWD || process.cwd();
15
+
16
+ function readStdinJson() {
17
+ try {
18
+ const raw = readFileSync(0, "utf8");
19
+ return raw.trim() ? JSON.parse(raw) : {};
20
+ } catch {
21
+ return {};
22
+ }
23
+ }
24
+
25
+ function git(args) {
26
+ try {
27
+ return execFileSync("git", args, {
28
+ cwd: PROJECT_ROOT,
29
+ encoding: "utf8",
30
+ stdio: ["ignore", "pipe", "ignore"],
31
+ timeout: 3000,
32
+ }).trim();
33
+ } catch {
34
+ return "";
35
+ }
36
+ }
37
+
38
+ function summarizeGit() {
39
+ const branch =
40
+ git(["branch", "--show-current"]) || git(["rev-parse", "--short", "HEAD"]);
41
+ const status = git(["status", "--short"]);
42
+ const rows = status ? status.split(/\r?\n/).filter(Boolean) : [];
43
+ return {
44
+ branch: branch || "unknown",
45
+ dirty: rows.length,
46
+ sample: rows.slice(0, 8),
47
+ };
48
+ }
49
+
50
+ function readJsonIfSmall(path, maxBytes = 24_000) {
51
+ try {
52
+ if (!existsSync(path)) return null;
53
+ if (statSync(path).size > maxBytes) return null;
54
+ return JSON.parse(readFileSync(path, "utf8"));
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ function summarizeModeState(root) {
61
+ const stateDir = join(root, ".omx", "state");
62
+ if (!existsSync(stateDir)) return [];
63
+ try {
64
+ return readdirSync(stateDir)
65
+ .filter((name) => name.endsWith("-state.json"))
66
+ .flatMap((name) => {
67
+ const state = readJsonIfSmall(join(stateDir, name));
68
+ if (!state || state.active === false) return [];
69
+ return [
70
+ {
71
+ mode: state.mode || name.replace(/-state\.json$/, ""),
72
+ phase: state.current_phase || state.phase || null,
73
+ active: state.active !== false,
74
+ },
75
+ ];
76
+ })
77
+ .slice(0, 8);
78
+ } catch {
79
+ return [];
80
+ }
81
+ }
82
+
83
+ function summarizeRetryState(input) {
84
+ const sessionId = input.session_id || input.sessionId || "";
85
+ if (!sessionId) return null;
86
+ const candidates = [
87
+ join(PROJECT_ROOT, ".omc", "state", `retry-${sessionId}.json`),
88
+ join(homedir(), ".omc", "state", `retry-${sessionId}.json`),
89
+ ];
90
+ for (const path of candidates) {
91
+ const state = readJsonIfSmall(path);
92
+ if (!state) continue;
93
+ return {
94
+ phase: state.phase || state.current_phase || null,
95
+ iteration: state.iteration ?? state.retry_count ?? null,
96
+ max: state.max_iterations ?? state.max ?? null,
97
+ };
98
+ }
99
+ return null;
100
+ }
101
+
102
+ function countStaleSwarmRuns(nowMs = Date.now()) {
103
+ const logsRoot = join(PROJECT_ROOT, ".triflux", "swarm-logs");
104
+ if (!existsSync(logsRoot)) return 0;
105
+ try {
106
+ return readdirSync(logsRoot, { withFileTypes: true }).filter((entry) => {
107
+ if (!entry.isDirectory() || !entry.name.startsWith("run-")) return false;
108
+ const eventPath = join(logsRoot, entry.name, "swarm-events.jsonl");
109
+ if (!existsSync(eventPath)) return false;
110
+ try {
111
+ return nowMs - statSync(eventPath).mtimeMs > STALE_RUN_MS;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }).length;
116
+ } catch {
117
+ return 0;
118
+ }
119
+ }
120
+
121
+ function truncateUtf8(text, maxBytes) {
122
+ let output = text;
123
+ while (Buffer.byteLength(output, "utf8") > maxBytes) {
124
+ output = output.slice(0, Math.max(0, output.length - 200));
125
+ }
126
+ return output;
127
+ }
128
+
129
+ export function buildSnapshot(input = readStdinJson()) {
130
+ const gitState = summarizeGit();
131
+ const activeModes = summarizeModeState(PROJECT_ROOT);
132
+ const retry = summarizeRetryState(input);
133
+ const staleSwarmRuns = countStaleSwarmRuns();
134
+
135
+ const lines = [
136
+ "[triflux pre-compact snapshot]",
137
+ `cwd: ${PROJECT_ROOT}`,
138
+ `git: ${gitState.branch}; dirty=${gitState.dirty}`,
139
+ ];
140
+ if (gitState.sample.length) {
141
+ lines.push(`dirty_sample: ${gitState.sample.join(" | ")}`);
142
+ }
143
+ if (activeModes.length) {
144
+ lines.push(
145
+ `active_modes: ${activeModes
146
+ .map((m) => `${m.mode}${m.phase ? `:${m.phase}` : ""}`)
147
+ .join(", ")}`,
148
+ );
149
+ }
150
+ if (retry) {
151
+ lines.push(
152
+ `retry: phase=${retry.phase || "-"} iteration=${retry.iteration ?? "-"}/${retry.max ?? "-"}`,
153
+ );
154
+ }
155
+ if (staleSwarmRuns > 0) {
156
+ lines.push(`stale_swarm_runs: ${staleSwarmRuns}`);
157
+ }
158
+
159
+ return truncateUtf8(lines.join("\n"), MAX_CONTEXT_BYTES);
160
+ }
161
+
162
+ function main() {
163
+ const snapshot = buildSnapshot();
164
+ if (!snapshot.trim()) return;
165
+ process.stdout.write(
166
+ JSON.stringify({
167
+ hookSpecificOutput: {
168
+ hookEventName: "PreCompact",
169
+ additionalContext: snapshot,
170
+ },
171
+ }),
172
+ );
173
+ }
174
+
175
+ try {
176
+ if (import.meta.url.endsWith(process.argv[1]?.split(/[\\/]/).pop() || "")) {
177
+ main();
178
+ }
179
+ } catch {
180
+ process.exit(0);
181
+ }
@@ -4,7 +4,7 @@
4
4
  // so the CLI works even when Hub is offline.
5
5
 
6
6
  import { execFile } from "node:child_process";
7
- import { existsSync, readFileSync } from "node:fs";
7
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
8
8
  import { homedir } from "node:os";
9
9
  import { join, resolve } from "node:path";
10
10
 
@@ -15,6 +15,15 @@ const DEFAULT_REGISTRY_CANDIDATES = [
15
15
  ".triflux/synapse/registry.json",
16
16
  join(homedir(), ".claude", "cache", "tfx-hub", "synapse-registry.json"),
17
17
  ];
18
+ const DEFAULT_SWARM_LOGS_DIR = ".triflux/swarm-logs";
19
+ const STALE_AFTER_MS = 30 * 60 * 1000;
20
+ const STATUS_PRIORITY = {
21
+ active: 3,
22
+ running: 3,
23
+ completed: 2,
24
+ failed: 2,
25
+ stale: 1,
26
+ };
18
27
 
19
28
  function gitExec(args, cwd) {
20
29
  return new Promise((res, rej) => {
@@ -55,10 +64,228 @@ function loadRegistrySnapshot(path) {
55
64
  }
56
65
  }
57
66
 
67
+ function parseJsonLines(path) {
68
+ try {
69
+ return readFileSync(path, "utf8")
70
+ .split(/\r?\n/)
71
+ .map((line) => line.trim())
72
+ .filter(Boolean)
73
+ .flatMap((line) => {
74
+ try {
75
+ return [JSON.parse(line)];
76
+ } catch {
77
+ return [];
78
+ }
79
+ });
80
+ } catch {
81
+ return [];
82
+ }
83
+ }
84
+
85
+ function normalizeRunId(name) {
86
+ return String(name || "").replace(/^run-/, "");
87
+ }
88
+
89
+ function eventTimeMs(event) {
90
+ const ms = Date.parse(String(event?.ts || ""));
91
+ return Number.isFinite(ms) ? ms : 0;
92
+ }
93
+
94
+ function deriveLogStatus(events, lastHeartbeatMs, nowMs) {
95
+ const stateEvents = events.filter((e) => e.event === "swarm_state");
96
+ const lastState = stateEvents[stateEvents.length - 1]?.to;
97
+ if (lastState === "completed" || lastState === "failed") return lastState;
98
+ if (events.some((e) => e.event === "integration_complete")) {
99
+ const lastIntegration = [...events]
100
+ .reverse()
101
+ .find((e) => e.event === "integration_complete");
102
+ if (
103
+ Array.isArray(lastIntegration?.failed) &&
104
+ lastIntegration.failed.length
105
+ ) {
106
+ return "failed";
107
+ }
108
+ return "completed";
109
+ }
110
+ if (nowMs - lastHeartbeatMs > STALE_AFTER_MS) return "stale";
111
+ return "active";
112
+ }
113
+
114
+ function buildLogSession(runDir, nowMs = Date.now()) {
115
+ const eventPath = join(runDir, "swarm-events.jsonl");
116
+ if (!existsSync(eventPath)) return null;
117
+ const events = parseJsonLines(eventPath);
118
+ if (events.length === 0) return null;
119
+
120
+ let statMs = 0;
121
+ try {
122
+ statMs = statSync(eventPath).mtimeMs;
123
+ } catch {}
124
+
125
+ const eventMs = events.map(eventTimeMs).filter((ms) => ms > 0);
126
+ const lastEventMs = eventMs.length ? Math.max(...eventMs) : statMs;
127
+ const launched = new Set();
128
+ const completed = new Set();
129
+ const failed = new Set();
130
+ for (const event of events) {
131
+ if (event.event === "shard_launched" && event.shard) {
132
+ launched.add(event.shard);
133
+ } else if (event.event === "shard_completed" && event.shard) {
134
+ completed.add(event.shard);
135
+ } else if (
136
+ (event.event === "shard_failed" ||
137
+ event.event === "shard_launch_failed") &&
138
+ event.shard
139
+ ) {
140
+ failed.add(event.shard);
141
+ }
142
+ }
143
+
144
+ const runId = normalizeRunId(runDir.split(/[\\/]/).pop());
145
+ const totalShards = launched.size || completed.size + failed.size;
146
+ const aliveWorkers = [...launched].filter(
147
+ (name) => !completed.has(name) && !failed.has(name),
148
+ ).length;
149
+
150
+ return {
151
+ sessionId: runId,
152
+ runId,
153
+ host: "-",
154
+ branch: "-",
155
+ dirtyFiles: [],
156
+ status: deriveLogStatus(events, lastEventMs || nowMs, nowMs),
157
+ taskSummary: "swarm log run",
158
+ source: "logs",
159
+ shards: totalShards ? `${completed.size}/${totalShards}` : "-",
160
+ workers: totalShards ? `${aliveWorkers} alive` : "-",
161
+ lastHeartbeat: lastEventMs || null,
162
+ logDir: runDir,
163
+ };
164
+ }
165
+
166
+ export function loadSwarmLogSessions(logsDir, opts = {}) {
167
+ const nowMs = opts.nowMs ?? Date.now();
168
+ const root = logsDir || DEFAULT_SWARM_LOGS_DIR;
169
+ if (!existsSync(root)) return [];
170
+ try {
171
+ return readdirSync(root, { withFileTypes: true })
172
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith("run-"))
173
+ .map((entry) => buildLogSession(join(root, entry.name), nowMs))
174
+ .filter(Boolean);
175
+ } catch {
176
+ return [];
177
+ }
178
+ }
179
+
180
+ function statusPriority(status) {
181
+ return STATUS_PRIORITY[status] || 0;
182
+ }
183
+
184
+ function mergeSession(existing, next) {
185
+ const status =
186
+ statusPriority(next.status) > statusPriority(existing.status)
187
+ ? next.status
188
+ : existing.status;
189
+ const source =
190
+ existing.source && next.source && existing.source !== next.source
191
+ ? "synapse + logs"
192
+ : existing.source || next.source || "synapse";
193
+ return {
194
+ ...next,
195
+ ...existing,
196
+ status,
197
+ source,
198
+ shards: next.shards || existing.shards,
199
+ workers: next.workers || existing.workers,
200
+ lastHeartbeat: Math.max(
201
+ Number(existing.lastHeartbeat || 0),
202
+ Number(next.lastHeartbeat || 0),
203
+ ),
204
+ logDir: existing.logDir || next.logDir,
205
+ };
206
+ }
207
+
208
+ export function mergeRegistryAndLogSessions(registrySessions, logSessions) {
209
+ const merged = new Map();
210
+ for (const session of registrySessions) {
211
+ const key = String(session.runId || session.sessionId || "");
212
+ if (!key) continue;
213
+ merged.set(key, {
214
+ ...session,
215
+ source: session.source || "synapse",
216
+ lastHeartbeat: session.lastHeartbeat || null,
217
+ });
218
+ }
219
+ for (const session of logSessions) {
220
+ const key = String(session.runId || session.sessionId || "");
221
+ if (!key) continue;
222
+ const existing = merged.get(key);
223
+ merged.set(key, existing ? mergeSession(existing, session) : session);
224
+ }
225
+ return [...merged.values()].sort((left, right) => {
226
+ const rightTs = Number(right.lastHeartbeat || 0);
227
+ const leftTs = Number(left.lastHeartbeat || 0);
228
+ return rightTs - leftTs;
229
+ });
230
+ }
231
+
232
+ function formatAge(ts, nowMs = Date.now()) {
233
+ if (!Number.isFinite(Number(ts)) || Number(ts) <= 0) return "-";
234
+ const seconds = Math.max(0, Math.round((nowMs - Number(ts)) / 1000));
235
+ if (seconds < 60) return `${seconds}s ago`;
236
+ const minutes = Math.round(seconds / 60);
237
+ if (minutes < 60) return `${minutes}m ago`;
238
+ const hours = Math.round(minutes / 60);
239
+ return `${hours}h ago`;
240
+ }
241
+
242
+ function hasSwarmLogFields(sessions) {
243
+ return sessions.some(
244
+ (s) =>
245
+ s.source === "logs" ||
246
+ s.source === "synapse + logs" ||
247
+ s.runId ||
248
+ s.shards ||
249
+ s.workers,
250
+ );
251
+ }
252
+
253
+ function formatSwarmStatus(sessions) {
254
+ const nowMs = Date.now();
255
+ const rows = [];
256
+ rows.push(
257
+ "RUN_ID STATUS SHARDS WORKERS LAST_HEARTBEAT SOURCE",
258
+ );
259
+ rows.push(
260
+ "───────────────────── ───────── ─────── ─────── ────────────── ──────────────",
261
+ );
262
+ for (const s of sessions) {
263
+ const id = String(s.runId || s.sessionId || "?")
264
+ .padEnd(21)
265
+ .slice(0, 21);
266
+ const status = String(s.status || "active")
267
+ .padEnd(9)
268
+ .slice(0, 9);
269
+ const shards = String(s.shards || "-")
270
+ .padEnd(7)
271
+ .slice(0, 7);
272
+ const workers = String(s.workers || "-")
273
+ .padEnd(7)
274
+ .slice(0, 7);
275
+ const heartbeat = formatAge(s.lastHeartbeat, nowMs).padEnd(14).slice(0, 14);
276
+ const source = String(s.source || "synapse").slice(0, 20);
277
+ rows.push(
278
+ `${id} ${status} ${shards} ${workers} ${heartbeat} ${source}`,
279
+ );
280
+ }
281
+ return rows.join("\n");
282
+ }
283
+
58
284
  function formatStatus(sessions) {
59
285
  if (!sessions.length) {
60
286
  return "no active sessions (synapse-registry empty)";
61
287
  }
288
+ if (hasSwarmLogFields(sessions)) return formatSwarmStatus(sessions);
62
289
  const rows = [];
63
290
  rows.push("SESSION HOST BRANCH DIRTY STATE TASK");
64
291
  rows.push(
@@ -84,13 +311,22 @@ function formatStatus(sessions) {
84
311
  export async function cmdSynapseStatus(args = [], opts = {}) {
85
312
  const jsonOut = args.includes("--json") || opts.json;
86
313
  const explicit = extractFlag(args, "--registry");
314
+ const explicitLogsDir = extractFlag(args, "--logs-dir");
87
315
  const path = locateRegistryPath(explicit);
88
- const sessions = loadRegistrySnapshot(path);
316
+ const registrySessions = loadRegistrySnapshot(path);
317
+ const logsDir = explicitLogsDir || DEFAULT_SWARM_LOGS_DIR;
318
+ const logSessions = loadSwarmLogSessions(logsDir);
319
+ const sessions = mergeRegistryAndLogSessions(registrySessions, logSessions);
89
320
 
90
321
  if (jsonOut) {
91
322
  process.stdout.write(
92
323
  JSON.stringify(
93
- { registry: path, count: sessions.length, sessions },
324
+ {
325
+ registry: path,
326
+ logsDir: existsSync(logsDir) ? logsDir : null,
327
+ count: sessions.length,
328
+ sessions,
329
+ },
94
330
  null,
95
331
  2,
96
332
  ) + "\n",
@@ -98,13 +334,14 @@ export async function cmdSynapseStatus(args = [], opts = {}) {
98
334
  return;
99
335
  }
100
336
 
101
- if (!path) {
337
+ if (!path && logSessions.length === 0) {
102
338
  process.stdout.write(
103
- "no registry file found (looked for .triflux/synapse-registry.json)\n",
339
+ "no registry or swarm logs found (looked for .triflux/synapse-registry.json and .triflux/swarm-logs)\n",
104
340
  );
105
341
  return;
106
342
  }
107
- process.stdout.write(`registry: ${path}\n`);
343
+ if (path) process.stdout.write(`registry: ${path}\n`);
344
+ if (logSessions.length > 0) process.stdout.write(`swarm logs: ${logsDir}\n`);
108
345
  process.stdout.write(`${formatStatus(sessions)}\n`);
109
346
  }
110
347