opencode-swarm-plugin 0.43.0 → 0.44.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/bin/cass.characterization.test.ts +422 -0
- package/bin/swarm.serve.test.ts +6 -4
- package/bin/swarm.test.ts +68 -0
- package/bin/swarm.ts +81 -8
- package/dist/compaction-prompt-scoring.js +139 -0
- package/dist/contributor-tools.d.ts +42 -0
- package/dist/contributor-tools.d.ts.map +1 -0
- package/dist/eval-capture.js +12811 -0
- package/dist/hive.d.ts.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7728 -62590
- package/dist/plugin.js +23833 -78695
- package/dist/sessions/agent-discovery.d.ts +59 -0
- package/dist/sessions/agent-discovery.d.ts.map +1 -0
- package/dist/sessions/index.d.ts +10 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm-review.d.ts.map +1 -1
- package/package.json +17 -5
- package/.changeset/swarm-insights-data-layer.md +0 -63
- package/.hive/analysis/eval-failure-analysis-2025-12-25.md +0 -331
- package/.hive/analysis/session-data-quality-audit.md +0 -320
- package/.hive/eval-results.json +0 -483
- package/.hive/issues.jsonl +0 -138
- package/.hive/memories.jsonl +0 -729
- package/.opencode/eval-history.jsonl +0 -327
- package/.turbo/turbo-build.log +0 -9
- package/CHANGELOG.md +0 -2255
- package/SCORER-ANALYSIS.md +0 -598
- package/docs/analysis/subagent-coordination-patterns.md +0 -902
- package/docs/analysis-socratic-planner-pattern.md +0 -504
- package/docs/planning/ADR-001-monorepo-structure.md +0 -171
- package/docs/planning/ADR-002-package-extraction.md +0 -393
- package/docs/planning/ADR-003-performance-improvements.md +0 -451
- package/docs/planning/ADR-004-message-queue-features.md +0 -187
- package/docs/planning/ADR-005-devtools-observability.md +0 -202
- package/docs/planning/ADR-007-swarm-enhancements-worktree-review.md +0 -168
- package/docs/planning/ADR-008-worker-handoff-protocol.md +0 -293
- package/docs/planning/ADR-009-oh-my-opencode-patterns.md +0 -353
- package/docs/planning/ROADMAP.md +0 -368
- package/docs/semantic-memory-cli-syntax.md +0 -123
- package/docs/swarm-mail-architecture.md +0 -1147
- package/docs/testing/context-recovery-test.md +0 -470
- package/evals/ARCHITECTURE.md +0 -1189
- package/evals/README.md +0 -768
- package/evals/compaction-prompt.eval.ts +0 -149
- package/evals/compaction-resumption.eval.ts +0 -289
- package/evals/coordinator-behavior.eval.ts +0 -307
- package/evals/coordinator-session.eval.ts +0 -154
- package/evals/evalite.config.ts.bak +0 -15
- package/evals/example.eval.ts +0 -31
- package/evals/fixtures/compaction-cases.ts +0 -350
- package/evals/fixtures/compaction-prompt-cases.ts +0 -311
- package/evals/fixtures/coordinator-sessions.ts +0 -328
- package/evals/fixtures/decomposition-cases.ts +0 -105
- package/evals/lib/compaction-loader.test.ts +0 -248
- package/evals/lib/compaction-loader.ts +0 -320
- package/evals/lib/data-loader.evalite-test.ts +0 -289
- package/evals/lib/data-loader.test.ts +0 -345
- package/evals/lib/data-loader.ts +0 -281
- package/evals/lib/llm.ts +0 -115
- package/evals/scorers/compaction-prompt-scorers.ts +0 -145
- package/evals/scorers/compaction-scorers.ts +0 -305
- package/evals/scorers/coordinator-discipline.evalite-test.ts +0 -539
- package/evals/scorers/coordinator-discipline.ts +0 -325
- package/evals/scorers/index.test.ts +0 -146
- package/evals/scorers/index.ts +0 -328
- package/evals/scorers/outcome-scorers.evalite-test.ts +0 -27
- package/evals/scorers/outcome-scorers.ts +0 -349
- package/evals/swarm-decomposition.eval.ts +0 -121
- package/examples/commands/swarm.md +0 -745
- package/examples/plugin-wrapper-template.ts +0 -2426
- package/examples/skills/hive-workflow/SKILL.md +0 -212
- package/examples/skills/skill-creator/SKILL.md +0 -223
- package/examples/skills/swarm-coordination/SKILL.md +0 -292
- package/global-skills/cli-builder/SKILL.md +0 -344
- package/global-skills/cli-builder/references/advanced-patterns.md +0 -244
- package/global-skills/learning-systems/SKILL.md +0 -644
- package/global-skills/skill-creator/LICENSE.txt +0 -202
- package/global-skills/skill-creator/SKILL.md +0 -352
- package/global-skills/skill-creator/references/output-patterns.md +0 -82
- package/global-skills/skill-creator/references/workflows.md +0 -28
- package/global-skills/swarm-coordination/SKILL.md +0 -995
- package/global-skills/swarm-coordination/references/coordinator-patterns.md +0 -235
- package/global-skills/swarm-coordination/references/strategies.md +0 -138
- package/global-skills/system-design/SKILL.md +0 -213
- package/global-skills/testing-patterns/SKILL.md +0 -430
- package/global-skills/testing-patterns/references/dependency-breaking-catalog.md +0 -586
- package/opencode-swarm-plugin-0.30.7.tgz +0 -0
- package/opencode-swarm-plugin-0.31.0.tgz +0 -0
- package/scripts/cleanup-test-memories.ts +0 -346
- package/scripts/init-skill.ts +0 -222
- package/scripts/migrate-unknown-sessions.ts +0 -349
- package/scripts/validate-skill.ts +0 -204
- package/src/agent-mail.ts +0 -1724
- package/src/anti-patterns.test.ts +0 -1167
- package/src/anti-patterns.ts +0 -448
- package/src/compaction-capture.integration.test.ts +0 -257
- package/src/compaction-hook.test.ts +0 -838
- package/src/compaction-hook.ts +0 -1204
- package/src/compaction-observability.integration.test.ts +0 -139
- package/src/compaction-observability.test.ts +0 -187
- package/src/compaction-observability.ts +0 -324
- package/src/compaction-prompt-scorers.test.ts +0 -475
- package/src/compaction-prompt-scoring.ts +0 -300
- package/src/dashboard.test.ts +0 -611
- package/src/dashboard.ts +0 -462
- package/src/error-enrichment.test.ts +0 -403
- package/src/error-enrichment.ts +0 -219
- package/src/eval-capture.test.ts +0 -1015
- package/src/eval-capture.ts +0 -929
- package/src/eval-gates.test.ts +0 -306
- package/src/eval-gates.ts +0 -218
- package/src/eval-history.test.ts +0 -508
- package/src/eval-history.ts +0 -214
- package/src/eval-learning.test.ts +0 -378
- package/src/eval-learning.ts +0 -360
- package/src/eval-runner.test.ts +0 -223
- package/src/eval-runner.ts +0 -402
- package/src/export-tools.test.ts +0 -476
- package/src/export-tools.ts +0 -257
- package/src/hive.integration.test.ts +0 -2241
- package/src/hive.ts +0 -1628
- package/src/index.ts +0 -935
- package/src/learning.integration.test.ts +0 -1815
- package/src/learning.ts +0 -1079
- package/src/logger.test.ts +0 -189
- package/src/logger.ts +0 -135
- package/src/mandate-promotion.test.ts +0 -473
- package/src/mandate-promotion.ts +0 -239
- package/src/mandate-storage.integration.test.ts +0 -601
- package/src/mandate-storage.test.ts +0 -578
- package/src/mandate-storage.ts +0 -794
- package/src/mandates.ts +0 -540
- package/src/memory-tools.test.ts +0 -195
- package/src/memory-tools.ts +0 -344
- package/src/memory.integration.test.ts +0 -334
- package/src/memory.test.ts +0 -158
- package/src/memory.ts +0 -527
- package/src/model-selection.test.ts +0 -188
- package/src/model-selection.ts +0 -68
- package/src/observability-tools.test.ts +0 -359
- package/src/observability-tools.ts +0 -871
- package/src/output-guardrails.test.ts +0 -438
- package/src/output-guardrails.ts +0 -381
- package/src/pattern-maturity.test.ts +0 -1160
- package/src/pattern-maturity.ts +0 -525
- package/src/planning-guardrails.test.ts +0 -491
- package/src/planning-guardrails.ts +0 -438
- package/src/plugin.ts +0 -23
- package/src/post-compaction-tracker.test.ts +0 -251
- package/src/post-compaction-tracker.ts +0 -237
- package/src/query-tools.test.ts +0 -636
- package/src/query-tools.ts +0 -324
- package/src/rate-limiter.integration.test.ts +0 -466
- package/src/rate-limiter.ts +0 -774
- package/src/replay-tools.test.ts +0 -496
- package/src/replay-tools.ts +0 -240
- package/src/repo-crawl.integration.test.ts +0 -441
- package/src/repo-crawl.ts +0 -610
- package/src/schemas/cell-events.test.ts +0 -347
- package/src/schemas/cell-events.ts +0 -807
- package/src/schemas/cell.ts +0 -257
- package/src/schemas/evaluation.ts +0 -166
- package/src/schemas/index.test.ts +0 -199
- package/src/schemas/index.ts +0 -286
- package/src/schemas/mandate.ts +0 -232
- package/src/schemas/swarm-context.ts +0 -115
- package/src/schemas/task.ts +0 -161
- package/src/schemas/worker-handoff.test.ts +0 -302
- package/src/schemas/worker-handoff.ts +0 -131
- package/src/skills.integration.test.ts +0 -1192
- package/src/skills.test.ts +0 -643
- package/src/skills.ts +0 -1549
- package/src/storage.integration.test.ts +0 -341
- package/src/storage.ts +0 -884
- package/src/structured.integration.test.ts +0 -817
- package/src/structured.test.ts +0 -1046
- package/src/structured.ts +0 -762
- package/src/swarm-decompose.test.ts +0 -188
- package/src/swarm-decompose.ts +0 -1302
- package/src/swarm-deferred.integration.test.ts +0 -157
- package/src/swarm-deferred.test.ts +0 -38
- package/src/swarm-insights.test.ts +0 -214
- package/src/swarm-insights.ts +0 -459
- package/src/swarm-mail.integration.test.ts +0 -970
- package/src/swarm-mail.ts +0 -739
- package/src/swarm-orchestrate.integration.test.ts +0 -282
- package/src/swarm-orchestrate.test.ts +0 -548
- package/src/swarm-orchestrate.ts +0 -3084
- package/src/swarm-prompts.test.ts +0 -1270
- package/src/swarm-prompts.ts +0 -2077
- package/src/swarm-research.integration.test.ts +0 -701
- package/src/swarm-research.test.ts +0 -698
- package/src/swarm-research.ts +0 -472
- package/src/swarm-review.integration.test.ts +0 -285
- package/src/swarm-review.test.ts +0 -879
- package/src/swarm-review.ts +0 -709
- package/src/swarm-strategies.ts +0 -407
- package/src/swarm-worktree.test.ts +0 -501
- package/src/swarm-worktree.ts +0 -575
- package/src/swarm.integration.test.ts +0 -2377
- package/src/swarm.ts +0 -38
- package/src/tool-adapter.integration.test.ts +0 -1221
- package/src/tool-availability.ts +0 -461
- package/tsconfig.json +0 -28
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* CASS Binary Characterization Tests
|
|
4
|
+
*
|
|
5
|
+
* These tests capture the CURRENT behavior of the CASS binary tools.
|
|
6
|
+
* They document WHAT the binary DOES, not what it SHOULD do.
|
|
7
|
+
*
|
|
8
|
+
* Purpose: Enable safe refactoring during ADR-010 (CASS inhousing).
|
|
9
|
+
* Our inhouse implementation must match these behaviors exactly.
|
|
10
|
+
*
|
|
11
|
+
* Pattern: Feathers Characterization Testing
|
|
12
|
+
* 1. Write a test you KNOW will fail
|
|
13
|
+
* 2. Run it - let the failure tell you actual behavior
|
|
14
|
+
* 3. Change the test to expect actual behavior
|
|
15
|
+
* 4. Repeat until you've characterized the code
|
|
16
|
+
*
|
|
17
|
+
* DO NOT modify these tests to match desired behavior.
|
|
18
|
+
* These are BASELINE tests - they verify behaviors ARE present.
|
|
19
|
+
*/
|
|
20
|
+
import { describe, test, expect } from "bun:test";
|
|
21
|
+
import { $ } from "bun";
|
|
22
|
+
import {
|
|
23
|
+
cassStatsBaseline,
|
|
24
|
+
cassSearchBaseline,
|
|
25
|
+
cassHealthHumanBaseline,
|
|
26
|
+
cassStatsHumanBaseline,
|
|
27
|
+
cassViewBaseline,
|
|
28
|
+
cassErrorBaseline,
|
|
29
|
+
type CassStatsResponse,
|
|
30
|
+
type CassSearchResponse,
|
|
31
|
+
} from "../evals/fixtures/cass-baseline.ts";
|
|
32
|
+
|
|
33
|
+
describe("CASS Binary - cass stats", () => {
|
|
34
|
+
test("JSON output structure matches baseline", async () => {
|
|
35
|
+
// CHARACTERIZATION: This documents the actual JSON structure
|
|
36
|
+
const result = await $`cass stats --json`.quiet().json();
|
|
37
|
+
|
|
38
|
+
// Verify top-level structure
|
|
39
|
+
expect(result).toHaveProperty("by_agent");
|
|
40
|
+
expect(result).toHaveProperty("conversations");
|
|
41
|
+
expect(result).toHaveProperty("date_range");
|
|
42
|
+
expect(result).toHaveProperty("db_path");
|
|
43
|
+
expect(result).toHaveProperty("messages");
|
|
44
|
+
expect(result).toHaveProperty("top_workspaces");
|
|
45
|
+
|
|
46
|
+
// Verify by_agent structure
|
|
47
|
+
expect(Array.isArray(result.by_agent)).toBe(true);
|
|
48
|
+
if (result.by_agent.length > 0) {
|
|
49
|
+
const firstAgent = result.by_agent[0];
|
|
50
|
+
expect(firstAgent).toHaveProperty("agent");
|
|
51
|
+
expect(firstAgent).toHaveProperty("count");
|
|
52
|
+
expect(typeof firstAgent.agent).toBe("string");
|
|
53
|
+
expect(typeof firstAgent.count).toBe("number");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Verify date_range structure
|
|
57
|
+
expect(result.date_range).toHaveProperty("newest");
|
|
58
|
+
expect(result.date_range).toHaveProperty("oldest");
|
|
59
|
+
expect(typeof result.date_range.newest).toBe("string");
|
|
60
|
+
expect(typeof result.date_range.oldest).toBe("string");
|
|
61
|
+
|
|
62
|
+
// Verify top_workspaces structure
|
|
63
|
+
expect(Array.isArray(result.top_workspaces)).toBe(true);
|
|
64
|
+
if (result.top_workspaces.length > 0) {
|
|
65
|
+
const firstWorkspace = result.top_workspaces[0];
|
|
66
|
+
expect(firstWorkspace).toHaveProperty("count");
|
|
67
|
+
expect(firstWorkspace).toHaveProperty("workspace");
|
|
68
|
+
expect(typeof firstWorkspace.count).toBe("number");
|
|
69
|
+
expect(typeof firstWorkspace.workspace).toBe("string");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Verify numeric fields are numbers
|
|
73
|
+
expect(typeof result.conversations).toBe("number");
|
|
74
|
+
expect(typeof result.messages).toBe("number");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("human-readable output format matches baseline", async () => {
|
|
78
|
+
// CHARACTERIZATION: This documents the actual human-readable format
|
|
79
|
+
const result = await $`cass stats`.quiet().text();
|
|
80
|
+
|
|
81
|
+
// Verify presence of key sections (order matters)
|
|
82
|
+
expect(result).toContain("CASS Index Statistics");
|
|
83
|
+
expect(result).toContain("Database:");
|
|
84
|
+
expect(result).toContain("Totals:");
|
|
85
|
+
expect(result).toContain("Conversations:");
|
|
86
|
+
expect(result).toContain("Messages:");
|
|
87
|
+
expect(result).toContain("By Agent:");
|
|
88
|
+
expect(result).toContain("Top Workspaces:");
|
|
89
|
+
expect(result).toContain("Date Range:");
|
|
90
|
+
|
|
91
|
+
// Verify format patterns
|
|
92
|
+
expect(result).toMatch(/Conversations: \d+/);
|
|
93
|
+
expect(result).toMatch(/Messages: \d+/);
|
|
94
|
+
expect(result).toMatch(/\w+: \d+/); // Agent counts
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("CASS Binary - cass search", () => {
|
|
99
|
+
test("JSON output structure matches baseline", async () => {
|
|
100
|
+
// CHARACTERIZATION: This documents the actual search response structure
|
|
101
|
+
const result = await $`cass search "test" --limit 2 --json`.quiet().json();
|
|
102
|
+
|
|
103
|
+
// Verify top-level structure
|
|
104
|
+
expect(result).toHaveProperty("count");
|
|
105
|
+
expect(result).toHaveProperty("cursor");
|
|
106
|
+
expect(result).toHaveProperty("hits");
|
|
107
|
+
expect(result).toHaveProperty("hits_clamped");
|
|
108
|
+
expect(result).toHaveProperty("limit");
|
|
109
|
+
expect(result).toHaveProperty("max_tokens");
|
|
110
|
+
expect(result).toHaveProperty("offset");
|
|
111
|
+
expect(result).toHaveProperty("query");
|
|
112
|
+
expect(result).toHaveProperty("request_id");
|
|
113
|
+
expect(result).toHaveProperty("total_matches");
|
|
114
|
+
|
|
115
|
+
// Verify types
|
|
116
|
+
expect(typeof result.count).toBe("number");
|
|
117
|
+
expect(typeof result.hits_clamped).toBe("boolean");
|
|
118
|
+
expect(typeof result.limit).toBe("number");
|
|
119
|
+
expect(typeof result.offset).toBe("number");
|
|
120
|
+
expect(typeof result.query).toBe("string");
|
|
121
|
+
expect(typeof result.total_matches).toBe("number");
|
|
122
|
+
expect(Array.isArray(result.hits)).toBe(true);
|
|
123
|
+
|
|
124
|
+
// Verify hit structure (if any hits returned)
|
|
125
|
+
if (result.hits.length > 0) {
|
|
126
|
+
const firstHit = result.hits[0];
|
|
127
|
+
expect(firstHit).toHaveProperty("agent");
|
|
128
|
+
expect(firstHit).toHaveProperty("content");
|
|
129
|
+
expect(firstHit).toHaveProperty("created_at");
|
|
130
|
+
expect(firstHit).toHaveProperty("line_number");
|
|
131
|
+
expect(firstHit).toHaveProperty("match_type");
|
|
132
|
+
expect(firstHit).toHaveProperty("score");
|
|
133
|
+
expect(firstHit).toHaveProperty("snippet");
|
|
134
|
+
expect(firstHit).toHaveProperty("source_path");
|
|
135
|
+
expect(firstHit).toHaveProperty("title");
|
|
136
|
+
expect(firstHit).toHaveProperty("workspace");
|
|
137
|
+
|
|
138
|
+
expect(typeof firstHit.agent).toBe("string");
|
|
139
|
+
expect(typeof firstHit.content).toBe("string");
|
|
140
|
+
expect(typeof firstHit.created_at).toBe("number");
|
|
141
|
+
expect(typeof firstHit.line_number).toBe("number");
|
|
142
|
+
expect(typeof firstHit.match_type).toBe("string");
|
|
143
|
+
expect(typeof firstHit.score).toBe("number");
|
|
144
|
+
expect(typeof firstHit.snippet).toBe("string");
|
|
145
|
+
expect(typeof firstHit.source_path).toBe("string");
|
|
146
|
+
expect(typeof firstHit.title).toBe("string");
|
|
147
|
+
expect(typeof firstHit.workspace).toBe("string");
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("query parameter is preserved in response", async () => {
|
|
152
|
+
// CHARACTERIZATION: Query echoed back in response
|
|
153
|
+
const testQuery = "characterization-test-query-12345";
|
|
154
|
+
const result = await $`cass search "${testQuery}" --json`.quiet().json();
|
|
155
|
+
|
|
156
|
+
expect(result.query).toBe(testQuery);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("limit parameter is respected", async () => {
|
|
160
|
+
// CHARACTERIZATION: Limit controls max hits returned
|
|
161
|
+
const result = await $`cass search "test" --limit 3 --json`.quiet().json();
|
|
162
|
+
|
|
163
|
+
expect(result.limit).toBe(3);
|
|
164
|
+
if (result.hits.length > 0) {
|
|
165
|
+
expect(result.hits.length).toBeLessThanOrEqual(3);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("empty results include suggestions field", async () => {
|
|
170
|
+
// CHARACTERIZATION: Empty results return suggestions
|
|
171
|
+
const result =
|
|
172
|
+
await $`cass search "xyzzy-nonexistent-term-99999" --json`.quiet().json();
|
|
173
|
+
|
|
174
|
+
if (result.total_matches === 0) {
|
|
175
|
+
// Empty results may include suggestions (optional feature)
|
|
176
|
+
// Just verify structure if present
|
|
177
|
+
if (result.suggestions) {
|
|
178
|
+
expect(Array.isArray(result.suggestions)).toBe(true);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("CASS Binary - cass health", () => {
|
|
185
|
+
test("health check returns status indicator", async () => {
|
|
186
|
+
// CHARACTERIZATION: Health check outputs status with timing
|
|
187
|
+
const result = await $`cass health`.quiet().text();
|
|
188
|
+
|
|
189
|
+
// Should contain status indicator (✓ or ✗)
|
|
190
|
+
const hasHealthyIndicator = result.includes("✓ Healthy");
|
|
191
|
+
const hasUnhealthyIndicator = result.includes("✗");
|
|
192
|
+
|
|
193
|
+
expect(hasHealthyIndicator || hasUnhealthyIndicator).toBe(true);
|
|
194
|
+
|
|
195
|
+
// Should include timing information
|
|
196
|
+
if (hasHealthyIndicator) {
|
|
197
|
+
expect(result).toMatch(/\(\d+ms\)/);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("health check may include staleness note", async () => {
|
|
202
|
+
// CHARACTERIZATION: May warn about stale index
|
|
203
|
+
const result = await $`cass health`.quiet().text();
|
|
204
|
+
|
|
205
|
+
// If index is stale, should mention it
|
|
206
|
+
// This is conditional - test just verifies format if present
|
|
207
|
+
if (result.includes("stale")) {
|
|
208
|
+
expect(result).toContain("Note:");
|
|
209
|
+
expect(result).toMatch(/older than \d+s/);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("health check exits with code 0 when healthy", async () => {
|
|
214
|
+
// CHARACTERIZATION: Exit code 0 = healthy
|
|
215
|
+
const proc = Bun.spawn(["cass", "health"], {
|
|
216
|
+
stdout: "pipe",
|
|
217
|
+
stderr: "pipe",
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const exitCode = await proc.exited;
|
|
221
|
+
// Exit code 0 means healthy (even if stale)
|
|
222
|
+
// Exit code 3 means missing index
|
|
223
|
+
expect([0, 3]).toContain(exitCode);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("CASS Binary - cass view", () => {
|
|
228
|
+
test("view output includes file path header", async () => {
|
|
229
|
+
// CHARACTERIZATION: View starts with file path
|
|
230
|
+
// We can only test this if session files exist
|
|
231
|
+
const sessionFiles = await $`ls ~/.config/swarm-tools/sessions/*.jsonl`
|
|
232
|
+
.quiet()
|
|
233
|
+
.text()
|
|
234
|
+
.catch(() => "");
|
|
235
|
+
|
|
236
|
+
if (sessionFiles.trim()) {
|
|
237
|
+
const firstFile = sessionFiles.split("\n")[0].trim();
|
|
238
|
+
const result = await $`cass view ${firstFile} -n 1`.quiet().text();
|
|
239
|
+
|
|
240
|
+
expect(result).toContain(`File: ${firstFile}`);
|
|
241
|
+
expect(result).toContain("Line: 1");
|
|
242
|
+
expect(result).toContain("context:");
|
|
243
|
+
expect(result).toContain("----------------------------------------");
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("view output shows line numbers with content", async () => {
|
|
248
|
+
// CHARACTERIZATION: Lines prefixed with numbers
|
|
249
|
+
const sessionFiles = await $`ls ~/.config/swarm-tools/sessions/*.jsonl`
|
|
250
|
+
.quiet()
|
|
251
|
+
.text()
|
|
252
|
+
.catch(() => "");
|
|
253
|
+
|
|
254
|
+
if (sessionFiles.trim()) {
|
|
255
|
+
const firstFile = sessionFiles.split("\n")[0].trim();
|
|
256
|
+
const result = await $`cass view ${firstFile} -n 1`.quiet().text();
|
|
257
|
+
|
|
258
|
+
// Target line marked with >
|
|
259
|
+
expect(result).toMatch(/>\s+\d+\s+\|/);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("view with non-existent file returns error", async () => {
|
|
264
|
+
// CHARACTERIZATION: File not found error structure
|
|
265
|
+
const result = await $`cass view /nonexistent/path.jsonl -n 1 --json`
|
|
266
|
+
.quiet()
|
|
267
|
+
.json()
|
|
268
|
+
.catch((e) => JSON.parse(e.stderr.toString()));
|
|
269
|
+
|
|
270
|
+
expect(result).toHaveProperty("error");
|
|
271
|
+
expect(result.error).toHaveProperty("code");
|
|
272
|
+
expect(result.error).toHaveProperty("kind");
|
|
273
|
+
expect(result.error).toHaveProperty("message");
|
|
274
|
+
expect(result.error).toHaveProperty("retryable");
|
|
275
|
+
|
|
276
|
+
expect(result.error.code).toBe(3);
|
|
277
|
+
expect(result.error.kind).toBe("file-not-found");
|
|
278
|
+
expect(result.error.retryable).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe("CASS Binary - Error handling", () => {
|
|
283
|
+
test("invalid arguments return usage error with hints", async () => {
|
|
284
|
+
// CHARACTERIZATION: Helpful error messages with examples
|
|
285
|
+
const proc = Bun.spawn(["cass", "stats", "--invalid-flag"], {
|
|
286
|
+
stdout: "pipe",
|
|
287
|
+
stderr: "pipe",
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
await proc.exited;
|
|
291
|
+
const stderr = await new Response(proc.stderr).text();
|
|
292
|
+
|
|
293
|
+
// Should contain helpful error information
|
|
294
|
+
expect(stderr).toBeTruthy();
|
|
295
|
+
// Typically shows usage or suggests --help
|
|
296
|
+
expect(stderr.toLowerCase()).toMatch(/usage|help|invalid|unexpected/);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("error responses include exit codes", async () => {
|
|
300
|
+
// CHARACTERIZATION: Exit codes documented in baseline
|
|
301
|
+
// Code 2 = usage error
|
|
302
|
+
// Code 3 = missing file/db
|
|
303
|
+
// Code 0 = success
|
|
304
|
+
|
|
305
|
+
const procInvalidArg = Bun.spawn(["cass", "stats", "--invalid-flag"], {
|
|
306
|
+
stdout: "pipe",
|
|
307
|
+
stderr: "pipe",
|
|
308
|
+
});
|
|
309
|
+
const exitCodeInvalid = await procInvalidArg.exited;
|
|
310
|
+
expect(exitCodeInvalid).toBe(2); // Usage error
|
|
311
|
+
|
|
312
|
+
const procSuccess = Bun.spawn(["cass", "stats", "--json"], {
|
|
313
|
+
stdout: "pipe",
|
|
314
|
+
stderr: "pipe",
|
|
315
|
+
});
|
|
316
|
+
const exitCodeSuccess = await procSuccess.exited;
|
|
317
|
+
expect([0, 3]).toContain(exitCodeSuccess); // Success or missing index
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe("CASS Binary - Robot mode documentation", () => {
|
|
322
|
+
test("--robot-help provides machine-readable documentation", async () => {
|
|
323
|
+
// CHARACTERIZATION: Robot help is designed for AI agents
|
|
324
|
+
const result = await $`cass --robot-help`.quiet().text();
|
|
325
|
+
|
|
326
|
+
expect(result).toContain("cass --robot-help (contract v1)");
|
|
327
|
+
expect(result).toContain("QUICKSTART (for AI agents):");
|
|
328
|
+
expect(result).toContain("TIME FILTERS:");
|
|
329
|
+
expect(result).toContain("WORKFLOW:");
|
|
330
|
+
expect(result).toContain("OUTPUT:");
|
|
331
|
+
expect(result).toContain("Exit codes:");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("robot-docs subcommand exists", async () => {
|
|
335
|
+
// CHARACTERIZATION: robot-docs provides detailed docs for AI
|
|
336
|
+
const result = await $`cass robot-docs commands`.quiet().text();
|
|
337
|
+
|
|
338
|
+
// Should return command documentation (exact format may vary)
|
|
339
|
+
expect(result.length).toBeGreaterThan(0);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe("CASS Binary - Flag behavior", () => {
|
|
344
|
+
test("--json flag produces machine-readable output", async () => {
|
|
345
|
+
// CHARACTERIZATION: --json enables JSON mode
|
|
346
|
+
const result = await $`cass stats --json`.quiet().text();
|
|
347
|
+
|
|
348
|
+
// Should be valid JSON
|
|
349
|
+
expect(() => JSON.parse(result)).not.toThrow();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("--json and --robot are equivalent", async () => {
|
|
353
|
+
// CHARACTERIZATION: Both flags enable robot mode
|
|
354
|
+
const jsonResult = await $`cass search "test" --limit 1 --json`
|
|
355
|
+
.quiet()
|
|
356
|
+
.json();
|
|
357
|
+
const robotResult = await $`cass search "test" --limit 1 --robot`
|
|
358
|
+
.quiet()
|
|
359
|
+
.json();
|
|
360
|
+
|
|
361
|
+
// Both should return same structure
|
|
362
|
+
expect(jsonResult).toHaveProperty("hits");
|
|
363
|
+
expect(robotResult).toHaveProperty("hits");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("limit flag controls result count", async () => {
|
|
367
|
+
// CHARACTERIZATION: --limit parameter
|
|
368
|
+
const result = await $`cass search "test" --limit 1 --json`.quiet().json();
|
|
369
|
+
|
|
370
|
+
expect(result.limit).toBe(1);
|
|
371
|
+
expect(result.hits.length).toBeLessThanOrEqual(1);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* CHARACTERIZATION NOTES:
|
|
377
|
+
*
|
|
378
|
+
* These tests document the following CASS binary behaviors:
|
|
379
|
+
*
|
|
380
|
+
* 1. JSON Output Structure:
|
|
381
|
+
* - cass stats: by_agent[], conversations, date_range, messages, top_workspaces[]
|
|
382
|
+
* - cass search: count, cursor, hits[], limit, offset, query, total_matches
|
|
383
|
+
* - Hit objects: agent, content, created_at, line_number, score, source_path, etc.
|
|
384
|
+
*
|
|
385
|
+
* 2. Human-Readable Output:
|
|
386
|
+
* - Formatted tables with headers
|
|
387
|
+
* - Numeric statistics
|
|
388
|
+
* - Date ranges
|
|
389
|
+
*
|
|
390
|
+
* 3. Error Handling:
|
|
391
|
+
* - Exit code 0 = success
|
|
392
|
+
* - Exit code 2 = usage error
|
|
393
|
+
* - Exit code 3 = missing file/db
|
|
394
|
+
* - Error objects with code, kind, message, retryable fields
|
|
395
|
+
*
|
|
396
|
+
* 4. Robot Mode:
|
|
397
|
+
* - --json and --robot flags are equivalent
|
|
398
|
+
* - --robot-help provides AI-friendly documentation
|
|
399
|
+
* - robot-docs subcommand for detailed docs
|
|
400
|
+
*
|
|
401
|
+
* 5. Search Behavior:
|
|
402
|
+
* - Query parameter echoed in response
|
|
403
|
+
* - Limit parameter controls max hits
|
|
404
|
+
* - Empty results may include suggestions
|
|
405
|
+
*
|
|
406
|
+
* 6. View Behavior:
|
|
407
|
+
* - File path header
|
|
408
|
+
* - Line numbers with > indicator for target
|
|
409
|
+
* - Context window (default 5 lines)
|
|
410
|
+
* - Horizontal separators
|
|
411
|
+
*
|
|
412
|
+
* 7. Health Check:
|
|
413
|
+
* - Status indicator (✓ or ✗)
|
|
414
|
+
* - Timing in milliseconds
|
|
415
|
+
* - Optional staleness warning
|
|
416
|
+
*
|
|
417
|
+
* When implementing the inhouse version:
|
|
418
|
+
* - Match these structures exactly
|
|
419
|
+
* - Preserve field names and types
|
|
420
|
+
* - Maintain error response format
|
|
421
|
+
* - Keep exit codes consistent
|
|
422
|
+
*/
|
package/bin/swarm.serve.test.ts
CHANGED
|
@@ -11,18 +11,18 @@ describe("swarm serve command", () => {
|
|
|
11
11
|
const args = ["serve", "--port", "8080"];
|
|
12
12
|
const port = args.includes("--port")
|
|
13
13
|
? Number.parseInt(args[args.indexOf("--port") + 1])
|
|
14
|
-
:
|
|
14
|
+
: 4483;
|
|
15
15
|
|
|
16
16
|
expect(port).toBe(8080);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
test("serve command defaults to port
|
|
19
|
+
test("serve command defaults to port 4483 (HIVE on phone keypad)", () => {
|
|
20
20
|
const args = ["serve"];
|
|
21
21
|
const port = args.includes("--port")
|
|
22
22
|
? Number.parseInt(args[args.indexOf("--port") + 1])
|
|
23
|
-
:
|
|
23
|
+
: 4483;
|
|
24
24
|
|
|
25
|
-
expect(port).toBe(
|
|
25
|
+
expect(port).toBe(4483);
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
test("serve command uses project path from CWD", () => {
|
|
@@ -32,7 +32,9 @@ describe("swarm serve command", () => {
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
test("serve command appears in help text", async () => {
|
|
35
|
+
const packageDir = import.meta.dir.replace("/bin", "");
|
|
35
36
|
const proc = spawn(["bun", "run", "bin/swarm.ts", "help"], {
|
|
37
|
+
cwd: packageDir,
|
|
36
38
|
stdout: "pipe",
|
|
37
39
|
stderr: "pipe",
|
|
38
40
|
});
|
package/bin/swarm.test.ts
CHANGED
|
@@ -1938,6 +1938,74 @@ describe("swarm replay", () => {
|
|
|
1938
1938
|
});
|
|
1939
1939
|
});
|
|
1940
1940
|
|
|
1941
|
+
describe("swarm viz", () => {
|
|
1942
|
+
test("parses port flag", () => {
|
|
1943
|
+
function parseVizArgs(args: string[]): { port: number } {
|
|
1944
|
+
let port = 4483; // HIVE on phone keypad
|
|
1945
|
+
|
|
1946
|
+
for (let i = 0; i < args.length; i++) {
|
|
1947
|
+
if (args[i] === "--port") {
|
|
1948
|
+
const portNum = Number.parseInt(args[i + 1]);
|
|
1949
|
+
if (!isNaN(portNum) && portNum > 0) {
|
|
1950
|
+
port = portNum;
|
|
1951
|
+
}
|
|
1952
|
+
i++;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
return { port };
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
const result = parseVizArgs(["--port", "8080"]);
|
|
1960
|
+
|
|
1961
|
+
expect(result.port).toBe(8080);
|
|
1962
|
+
});
|
|
1963
|
+
|
|
1964
|
+
test("defaults to port 4483 (HIVE)", () => {
|
|
1965
|
+
function parseVizArgs(args: string[]): { port: number } {
|
|
1966
|
+
let port = 4483;
|
|
1967
|
+
|
|
1968
|
+
for (let i = 0; i < args.length; i++) {
|
|
1969
|
+
if (args[i] === "--port") {
|
|
1970
|
+
const portNum = Number.parseInt(args[i + 1]);
|
|
1971
|
+
if (!isNaN(portNum) && portNum > 0) {
|
|
1972
|
+
port = portNum;
|
|
1973
|
+
}
|
|
1974
|
+
i++;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
return { port };
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
const result = parseVizArgs([]);
|
|
1982
|
+
|
|
1983
|
+
expect(result.port).toBe(4483);
|
|
1984
|
+
});
|
|
1985
|
+
|
|
1986
|
+
test("ignores invalid port values", () => {
|
|
1987
|
+
function parseVizArgs(args: string[]): { port: number } {
|
|
1988
|
+
let port = 4483;
|
|
1989
|
+
|
|
1990
|
+
for (let i = 0; i < args.length; i++) {
|
|
1991
|
+
if (args[i] === "--port") {
|
|
1992
|
+
const portNum = Number.parseInt(args[i + 1]);
|
|
1993
|
+
if (!isNaN(portNum) && portNum > 0) {
|
|
1994
|
+
port = portNum;
|
|
1995
|
+
}
|
|
1996
|
+
i++;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
return { port };
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
const result = parseVizArgs(["--port", "invalid"]);
|
|
2004
|
+
|
|
2005
|
+
expect(result.port).toBe(4483); // Falls back to default
|
|
2006
|
+
});
|
|
2007
|
+
});
|
|
2008
|
+
|
|
1941
2009
|
describe("swarm export", () => {
|
|
1942
2010
|
test("parses format flag", () => {
|
|
1943
2011
|
function parseExportArgs(args: string[]): {
|
package/bin/swarm.ts
CHANGED
|
@@ -2917,8 +2917,10 @@ ${cyan("Commands:")}
|
|
|
2917
2917
|
swarm config Show paths to generated config files
|
|
2918
2918
|
swarm agents Update AGENTS.md with skill awareness
|
|
2919
2919
|
swarm migrate Migrate PGlite database to libSQL
|
|
2920
|
-
swarm serve Start SSE server for real-time event streaming
|
|
2921
|
-
--port <n> Port to listen on (default:
|
|
2920
|
+
swarm serve Start SSE server for real-time event streaming (port 4483 - HIVE)
|
|
2921
|
+
--port <n> Port to listen on (default: 4483)
|
|
2922
|
+
swarm viz Alias for 'swarm serve' (deprecated, use serve)
|
|
2923
|
+
--port <n> Port to listen on (default: 4483)
|
|
2922
2924
|
swarm cells List or get cells from database (replaces 'swarm tool hive_query')
|
|
2923
2925
|
swarm log View swarm logs with filtering
|
|
2924
2926
|
swarm stats Show swarm health metrics powered by swarm-insights (strategy success rates, patterns)
|
|
@@ -4919,21 +4921,21 @@ async function evalRun() {
|
|
|
4919
4921
|
async function serve() {
|
|
4920
4922
|
p.intro("swarm serve v" + VERSION);
|
|
4921
4923
|
|
|
4922
|
-
// Parse --port flag (default
|
|
4924
|
+
// Parse --port flag (default 4483 - HIVE on phone keypad)
|
|
4923
4925
|
const portFlagIndex = process.argv.indexOf("--port");
|
|
4924
4926
|
const port = portFlagIndex !== -1
|
|
4925
|
-
? Number.parseInt(process.argv[portFlagIndex + 1]) ||
|
|
4926
|
-
:
|
|
4927
|
+
? Number.parseInt(process.argv[portFlagIndex + 1]) || 4483
|
|
4928
|
+
: 4483;
|
|
4927
4929
|
|
|
4928
4930
|
const projectPath = process.cwd();
|
|
4929
4931
|
|
|
4930
4932
|
p.log.step("Starting DurableStreamServer...");
|
|
4931
4933
|
p.log.message(dim(` Project: ${projectPath}`));
|
|
4932
|
-
p.log.message(dim(` Port: ${port}`));
|
|
4934
|
+
p.log.message(dim(` Port: ${port} (HIVE on phone keypad)`));
|
|
4933
4935
|
|
|
4934
4936
|
try {
|
|
4935
4937
|
// Import dependencies
|
|
4936
|
-
const { getSwarmMailLibSQL } = await import("swarm-mail");
|
|
4938
|
+
const { getSwarmMailLibSQL, createHiveAdapter } = await import("swarm-mail");
|
|
4937
4939
|
const { createDurableStreamAdapter, createDurableStreamServer } = await import("swarm-mail");
|
|
4938
4940
|
|
|
4939
4941
|
// Get swarm-mail adapter
|
|
@@ -4942,9 +4944,14 @@ async function serve() {
|
|
|
4942
4944
|
// Create stream adapter
|
|
4943
4945
|
const streamAdapter = createDurableStreamAdapter(swarmMail, projectPath);
|
|
4944
4946
|
|
|
4947
|
+
// Create hive adapter for cells endpoint
|
|
4948
|
+
const db = await swarmMail.getDatabase(projectPath);
|
|
4949
|
+
const hiveAdapter = createHiveAdapter(db, projectPath);
|
|
4950
|
+
|
|
4945
4951
|
// Create and start server
|
|
4946
4952
|
const server = createDurableStreamServer({
|
|
4947
4953
|
adapter: streamAdapter,
|
|
4954
|
+
hiveAdapter,
|
|
4948
4955
|
port,
|
|
4949
4956
|
projectKey: projectPath,
|
|
4950
4957
|
});
|
|
@@ -4953,8 +4960,9 @@ async function serve() {
|
|
|
4953
4960
|
|
|
4954
4961
|
p.log.success("Server started!");
|
|
4955
4962
|
p.log.message("");
|
|
4956
|
-
p.log.message(cyan(
|
|
4963
|
+
p.log.message(cyan(` Dashboard: http://localhost:5173`));
|
|
4957
4964
|
p.log.message(cyan(` SSE Endpoint: ${server.url}/streams/${encodeURIComponent(projectPath)}`));
|
|
4965
|
+
p.log.message(cyan(` Cells API: ${server.url}/cells`));
|
|
4958
4966
|
p.log.message("");
|
|
4959
4967
|
p.log.message(dim(" Press Ctrl+C to stop"));
|
|
4960
4968
|
|
|
@@ -4968,6 +4976,68 @@ async function serve() {
|
|
|
4968
4976
|
}
|
|
4969
4977
|
}
|
|
4970
4978
|
|
|
4979
|
+
// ============================================================================
|
|
4980
|
+
// Viz Command - Start Dashboard Server
|
|
4981
|
+
// ============================================================================
|
|
4982
|
+
|
|
4983
|
+
async function viz() {
|
|
4984
|
+
p.intro("swarm viz v" + VERSION);
|
|
4985
|
+
|
|
4986
|
+
// Parse --port flag (default 4483 - HIVE on phone keypad)
|
|
4987
|
+
const portFlagIndex = process.argv.indexOf("--port");
|
|
4988
|
+
const port = portFlagIndex !== -1
|
|
4989
|
+
? Number.parseInt(process.argv[portFlagIndex + 1]) || 4483
|
|
4990
|
+
: 4483;
|
|
4991
|
+
|
|
4992
|
+
const projectPath = process.cwd();
|
|
4993
|
+
|
|
4994
|
+
p.log.step("Starting dashboard server...");
|
|
4995
|
+
p.log.message(dim(` Project: ${projectPath}`));
|
|
4996
|
+
p.log.message(dim(` Port: ${port}`));
|
|
4997
|
+
|
|
4998
|
+
try {
|
|
4999
|
+
// Import dependencies
|
|
5000
|
+
const { getSwarmMailLibSQL, createHiveAdapter } = await import("swarm-mail");
|
|
5001
|
+
const { createDurableStreamAdapter, createDurableStreamServer } = await import("swarm-mail");
|
|
5002
|
+
|
|
5003
|
+
// Get swarm-mail adapter
|
|
5004
|
+
const swarmMail = await getSwarmMailLibSQL(projectPath);
|
|
5005
|
+
|
|
5006
|
+
// Create stream adapter
|
|
5007
|
+
const streamAdapter = createDurableStreamAdapter(swarmMail, projectPath);
|
|
5008
|
+
|
|
5009
|
+
// Create hive adapter for cells endpoint
|
|
5010
|
+
const db = await swarmMail.getDatabase(projectPath);
|
|
5011
|
+
const hiveAdapter = createHiveAdapter(db, projectPath);
|
|
5012
|
+
|
|
5013
|
+
// Create and start server
|
|
5014
|
+
const server = createDurableStreamServer({
|
|
5015
|
+
adapter: streamAdapter,
|
|
5016
|
+
hiveAdapter,
|
|
5017
|
+
port,
|
|
5018
|
+
projectKey: projectPath,
|
|
5019
|
+
});
|
|
5020
|
+
|
|
5021
|
+
await server.start();
|
|
5022
|
+
|
|
5023
|
+
p.log.success("Dashboard server running!");
|
|
5024
|
+
p.log.message("");
|
|
5025
|
+
p.log.message(cyan(` Dashboard: http://localhost:${port}`));
|
|
5026
|
+
p.log.message(cyan(` SSE endpoint: http://localhost:${port}/streams/${encodeURIComponent(projectPath)}`));
|
|
5027
|
+
p.log.message(cyan(` Cells API: http://localhost:${port}/cells`));
|
|
5028
|
+
p.log.message("");
|
|
5029
|
+
p.log.message(dim(" Press Ctrl+C to stop"));
|
|
5030
|
+
|
|
5031
|
+
// Keep process alive
|
|
5032
|
+
await new Promise(() => {});
|
|
5033
|
+
} catch (error) {
|
|
5034
|
+
p.log.error("Failed to start dashboard server");
|
|
5035
|
+
p.log.message(error instanceof Error ? error.message : String(error));
|
|
5036
|
+
p.outro("Aborted");
|
|
5037
|
+
process.exit(1);
|
|
5038
|
+
}
|
|
5039
|
+
}
|
|
5040
|
+
|
|
4971
5041
|
// ============================================================================
|
|
4972
5042
|
// Main
|
|
4973
5043
|
// ============================================================================
|
|
@@ -4993,6 +5063,9 @@ switch (command) {
|
|
|
4993
5063
|
case "serve":
|
|
4994
5064
|
await serve();
|
|
4995
5065
|
break;
|
|
5066
|
+
case "viz":
|
|
5067
|
+
await viz();
|
|
5068
|
+
break;
|
|
4996
5069
|
case "update":
|
|
4997
5070
|
await update();
|
|
4998
5071
|
break;
|