@funara/wevr 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +397 -0
  3. package/bin/wevr.js +4 -0
  4. package/package.json +48 -0
  5. package/src/cli/commands/doctor.js +137 -0
  6. package/src/cli/commands/init.js +156 -0
  7. package/src/cli/commands/launch.js +122 -0
  8. package/src/cli/commands/theme.js +67 -0
  9. package/src/cli/commands/theme.test.js +28 -0
  10. package/src/cli/commands/uninstall.js +103 -0
  11. package/src/cli/commands/update.js +9 -0
  12. package/src/cli/index.js +63 -0
  13. package/src/cli/wizard/selectModelTier.js +40 -0
  14. package/src/core/agentPromptWriter.js +45 -0
  15. package/src/core/agentPromptWriter.test.js +56 -0
  16. package/src/core/backup.js +46 -0
  17. package/src/core/backup.test.js +51 -0
  18. package/src/core/commandsWriter.js +26 -0
  19. package/src/core/commandsWriter.test.js +29 -0
  20. package/src/core/configBuilder.js +32 -0
  21. package/src/core/configBuilder.test.js +93 -0
  22. package/src/core/configWriter.js +10 -0
  23. package/src/core/configWriter.test.js +26 -0
  24. package/src/core/identityHeader.js +8 -0
  25. package/src/core/identityHeader.test.js +15 -0
  26. package/src/core/paths.js +13 -0
  27. package/src/core/paths.test.js +33 -0
  28. package/src/core/pluginWriter.js +29 -0
  29. package/src/core/pluginWriter.test.js +41 -0
  30. package/src/core/skillsWriter.js +13 -0
  31. package/src/core/skillsWriter.test.js +30 -0
  32. package/src/core/themeWriter.js +26 -0
  33. package/src/core/themeWriter.test.js +29 -0
  34. package/src/core/tuiConfigWriter.js +22 -0
  35. package/src/core/tuiConfigWriter.test.js +38 -0
  36. package/src/core/version.js +8 -0
  37. package/src/core/versionCheck.js +44 -0
  38. package/src/core/versionCheck.test.js +34 -0
  39. package/src/plugins/README.md +57 -0
  40. package/src/plugins/wevr-flow.js +137 -0
  41. package/src/plugins/wevr-squeeze.js +3630 -0
  42. package/src/templates/agent-prompts/analyze.txt +43 -0
  43. package/src/templates/agent-prompts/builder.txt +10 -0
  44. package/src/templates/agent-prompts/compose.txt +45 -0
  45. package/src/templates/agent-prompts/debug.txt +43 -0
  46. package/src/templates/agent-prompts/explorer.txt +10 -0
  47. package/src/templates/agent-prompts/hierarchy.txt +95 -0
  48. package/src/templates/agent-prompts/reporter.txt +10 -0
  49. package/src/templates/agent-prompts/verifier.txt +10 -0
  50. package/src/templates/commands/squeeze-dashboard.md +5 -0
  51. package/src/templates/commands/squeeze-health.md +10 -0
  52. package/src/templates/commands/squeeze-quick.md +10 -0
  53. package/src/templates/model-defaults.json +59 -0
  54. package/src/templates/opencode.config.json +243 -0
  55. package/src/templates/skills/brooks-lint-rca/SKILL.md +48 -0
  56. package/src/templates/skills/codebase-fact-finding/SKILL.md +39 -0
  57. package/src/templates/skills/diff-review/SKILL.md +42 -0
  58. package/src/templates/skills/general-coding/SKILL.md +43 -0
  59. package/src/templates/skills/minimal-fixing/SKILL.md +25 -0
  60. package/src/templates/skills/plan-checking/SKILL.md +33 -0
  61. package/src/templates/skills/ponytail-patching/SKILL.md +20 -0
  62. package/src/templates/skills/prd-formatting/SKILL.md +45 -0
  63. package/src/templates/skills/refactoring-patterns/SKILL.md +37 -0
  64. package/src/templates/skills/security-auditing/SKILL.md +35 -0
  65. package/src/templates/skills/security-remediation/SKILL.md +37 -0
  66. package/src/templates/skills/summary-reporting/SKILL.md +83 -0
  67. package/src/templates/skills/test-assurance/SKILL.md +44 -0
  68. package/src/templates/skills/test-mocking-strategy/SKILL.md +18 -0
  69. package/src/templates/skills/ui-design-audit/SKILL.md +23 -0
  70. package/src/templates/skills/ui-design-system/SKILL.md +37 -0
  71. package/src/templates/skills/wstg-recon/SKILL.md +33 -0
  72. package/src/templates/themes/wevr-colorful.json +241 -0
  73. package/src/templates/themes/wevr-dark.json +177 -0
  74. package/src/templates/themes/wevr-light.json +241 -0
@@ -0,0 +1,38 @@
1
+ import { describe, it, after } from "node:test"
2
+ import assert from "node:assert"
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
4
+ import { join } from "node:path"
5
+ import { tmpdir } from "node:os"
6
+ import { writeTuiConfig } from "./tuiConfigWriter.js"
7
+
8
+ describe("tuiConfigWriter", () => {
9
+ const destDir = mkdtempSync(join(tmpdir(), "wevr-tui-dest-"))
10
+ const tuiPath = join(destDir, "tui.json")
11
+
12
+ after(() => {
13
+ if (existsSync(destDir)) rmSync(destDir, { recursive: true })
14
+ })
15
+
16
+ it("writes new tui.json with theme name if it does not exist", () => {
17
+ writeTuiConfig("wevr-dark", tuiPath)
18
+
19
+ assert.ok(existsSync(tuiPath))
20
+ const parsed = JSON.parse(readFileSync(tuiPath, "utf-8"))
21
+ assert.strictEqual(parsed.theme, "wevr-dark")
22
+ assert.strictEqual(parsed["$schema"], "https://opencode.ai/tui.json")
23
+ })
24
+
25
+ it("updates existing tui.json theme name while preserving other fields", () => {
26
+ writeFileSync(tuiPath, JSON.stringify({
27
+ "$schema": "https://opencode.ai/tui.json",
28
+ "theme": "ayu-dark",
29
+ "font": "monolisa"
30
+ }, null, 2), "utf-8")
31
+
32
+ writeTuiConfig("wevr-dark", tuiPath)
33
+
34
+ const parsed = JSON.parse(readFileSync(tuiPath, "utf-8"))
35
+ assert.strictEqual(parsed.theme, "wevr-dark")
36
+ assert.strictEqual(parsed.font, "monolisa")
37
+ })
38
+ })
@@ -0,0 +1,8 @@
1
+ import { readFileSync } from "node:fs"
2
+ import { resolve, dirname } from "node:path"
3
+ import { fileURLToPath } from "node:url"
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url))
6
+ export const version = JSON.parse(
7
+ readFileSync(resolve(__dirname, "../../package.json"), "utf-8")
8
+ ).version
@@ -0,0 +1,44 @@
1
+ import { execFileSync, execSync } from "node:child_process"
2
+ import { confirm, isCancel } from "@clack/prompts"
3
+
4
+ export function fetchLatestVersion() {
5
+ try {
6
+ if (process.platform === "win32") {
7
+ return execSync("npm view wevr version", { stdio: "pipe", timeout: 1500 })
8
+ .toString()
9
+ .trim()
10
+ }
11
+ return execFileSync("npm", ["view", "wevr", "version"], { stdio: "pipe", timeout: 1500 })
12
+ .toString()
13
+ .trim()
14
+ } catch {
15
+ return null
16
+ }
17
+ }
18
+
19
+ export async function checkAndPromptUpdate(currentVersion) {
20
+ process.stdout.write("Checking for updates... ")
21
+
22
+ const latest = fetchLatestVersion()
23
+ if (!latest) {
24
+ console.log("⚠ Could not reach npm registry — skipping")
25
+ return false
26
+ }
27
+
28
+ if (latest === currentVersion) {
29
+ console.log(`✓ Up to date (${currentVersion})`)
30
+ return false
31
+ }
32
+
33
+ console.log(`⚠ wevr ${latest} available (you have ${currentVersion})`)
34
+
35
+ const answer = await confirm({ message: "Update now?", initialValue: true })
36
+ if (isCancel(answer) || !answer) return false
37
+
38
+ if (process.platform === "win32") {
39
+ execSync("npm install -g wevr", { stdio: "inherit" })
40
+ } else {
41
+ execFileSync("npm", ["install", "-g", "wevr"], { stdio: "inherit" })
42
+ }
43
+ return true
44
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, it } from "node:test"
2
+ import assert from "node:assert"
3
+ import { execFileSync, execSync } from "node:child_process"
4
+ import { fetchLatestVersion } from "./versionCheck.js"
5
+
6
+ describe("fetchLatestVersion", () => {
7
+ it("returns a string or null", () => {
8
+ // Either reaches npm registry (string) or fails gracefully (null)
9
+ const result = fetchLatestVersion()
10
+ assert.ok(result === null || typeof result === "string")
11
+ })
12
+
13
+ it("returns null when npm command throws", () => {
14
+ // Verify the try/catch in fetchLatestVersion swallows errors
15
+ let caught = null
16
+ try {
17
+ if (process.platform === "win32") {
18
+ execSync("npm view __nonexistent_pkg_xyz_wevr__ version", { stdio: "pipe" })
19
+ } else {
20
+ execFileSync("npm", ["view", "__nonexistent_pkg_xyz_wevr__", "version"], { stdio: "pipe" })
21
+ }
22
+ } catch (err) {
23
+ caught = err
24
+ }
25
+ // npm throws on unknown package — fetchLatestVersion handles this and returns null
26
+ assert.ok(caught instanceof Error, "npm should throw for unknown package")
27
+ })
28
+
29
+ it("uses execSync on Windows for npm, plain execFileSync on other platforms", () => {
30
+ // Confirm the platform guard evaluates correctly
31
+ const useSync = process.platform === "win32"
32
+ assert.strictEqual(typeof useSync, "boolean")
33
+ })
34
+ })
@@ -0,0 +1,57 @@
1
+ # Bundled plugins
2
+
3
+ This folder holds plugin source files that `wevr init` copies into the
4
+ user's OpenCode plugin directory (`~/.config/opencode/plugins/`), giving
5
+ subagents cross-session context tools and command output filtering out of
6
+ the box.
7
+
8
+ ## `wevr-flow.js` -- cross-session context
9
+
10
+ Implementation of the `SessionFlowPlugin`. Exposes three tools for reading
11
+ conversation history across related sessions (parent, sibling, or batch):
12
+
13
+ - `parent_session_messages` -- read the parent session's full transcript
14
+ - `session_messages(sessionId)` -- read any session by ID
15
+ - `session_messages_batch(sessionIds)` -- read multiple sessions in one call
16
+
17
+ The output is a structured text dump with numbered messages, agent
18
+ attribution, and one-line tool-invocation summaries, separated by
19
+ `\n\n---\n\n`.
20
+
21
+ ## `wevr-squeeze.js` -- context quality & session continuity
22
+
23
+ Implementation of the `WevrSqueezePlugin` (rebranded from `token-optimizer-opencode`). Monitors context fill limits and session health, calculates ResourceHealth and SessionEfficiency metrics, alerts on loop/retry patterns, and enables seamless session continuity/restores for same-project tasks.
24
+
25
+ ## Why `wevr-flow` was extracted
26
+
27
+ Subagents (Coder, Tester, Reviewer, Inspector, Fixer, etc.) run in fresh
28
+ child sessions with no access to the parent orchestrator's conversation
29
+ history. When a parent agent dispatches a subagent via the `Task` tool, the
30
+ subagent cannot see what was discussed in the parent -- which means it has
31
+ to ask the user to re-paste context, or guess. `parent_session_messages`
32
+ bridges that gap: the subagent can fetch the parent transcript on demand
33
+ and pick up where the parent left off.
34
+
35
+ ## Why the identity plugin was dropped
36
+
37
+ The original `AgentSelfIdentityPlugin` injected agent identity via
38
+ `experimental.chat.*` hooks. Wevr's 12 agent names are known at install
39
+ time, so we prepend a static identity header (`You are the "<name>" agent.`)
40
+ to every generated prompt file. This is KISS: no hook evaluation per
41
+ request, no dependency on undocumented experimental APIs, single source
42
+ of truth in `src/core/identityHeader.js`.
43
+
44
+ ## Why the attribution tool was dropped
45
+
46
+ The original `AgentAttributionToolPlugin` was designed for a Retrospective
47
+ agent that captures post-session observations. Wevr has no Retrospective
48
+ agent in its pipeline, so the tool would have no caller -- YAGNI.
49
+
50
+ ## How they are loaded
51
+
52
+ `wevr init` copies both plugins into `~/.config/opencode/plugins/` and
53
+ writes a `package.json` declaring `@opencode-ai/plugin: ^1.17.9` as a
54
+ dependency. On OpenCode's first launch, the
55
+ bundled Bun runtime detects the `package.json` and runs `bun install`
56
+ automatically -- no user action required. Once installed, both plugins are
57
+ available to every agent that has the appropriate permissions.
@@ -0,0 +1,137 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+
3
+ function formatInput(input) {
4
+ if (
5
+ input == null ||
6
+ (typeof input === "object" &&
7
+ !Array.isArray(input) &&
8
+ Object.keys(input).length === 0)
9
+ ) {
10
+ return ""
11
+ }
12
+ return `\n input: ${JSON.stringify(input)}`
13
+ }
14
+
15
+ function formatTool(part) {
16
+ const state = part.state
17
+ const status = state.status
18
+ const title = state.title ?? part.tool
19
+ const input = formatInput(state.input)
20
+ if (status === "error") {
21
+ return ` [tool] ${title} → error: ${state.error}${input}`
22
+ }
23
+ return ` [tool] ${title} → ${status}${input}`
24
+ }
25
+
26
+ function formatParts(parts) {
27
+ const lines = []
28
+ for (const part of parts) {
29
+ if (part.type === "text") {
30
+ lines.push(part.text)
31
+ } else if (part.type === "tool") {
32
+ lines.push(formatTool(part))
33
+ }
34
+ }
35
+ return lines.join("\n")
36
+ }
37
+
38
+ function formatModel(info) {
39
+ const base = `${info.providerID}/${info.modelID}`
40
+ return info.variant ? `${base} (${info.variant})` : base
41
+ }
42
+
43
+ function formatMessage(msg, index) {
44
+ const num = index + 1
45
+ if (msg.info.role === "assistant") {
46
+ const info = msg.info
47
+ const agent = info.agent ?? "unknown"
48
+ const header = `${num}. assistant (${agent}) [${formatModel(info)}]`
49
+ return `${header}\n${formatParts(msg.parts)}`
50
+ }
51
+ return `${num}. ${msg.info.role}\n${formatParts(msg.parts)}`
52
+ }
53
+
54
+ export const SessionFlowPlugin = async ({ client }) => {
55
+ async function formatSessionMessages(sessionID, emptyMessage) {
56
+ const response = await client.session.messages({
57
+ path: { id: sessionID },
58
+ })
59
+ const messages = response.data ?? []
60
+ if (messages.length === 0) {
61
+ return emptyMessage
62
+ }
63
+
64
+ return messages.map(formatMessage).join("\n\n---\n\n")
65
+ }
66
+
67
+ async function formatSessionMessagesBatch(sessionIDs) {
68
+ const sections = await Promise.all(
69
+ sessionIDs.map(async (sessionID) => {
70
+ const emptyMessage = "(No messages found or session not accessible)"
71
+
72
+ try {
73
+ const output = await formatSessionMessages(sessionID, emptyMessage)
74
+ return `=== Session: ${sessionID} ===\n${output}`
75
+ } catch {
76
+ return `=== Session: ${sessionID} ===\n${emptyMessage}`
77
+ }
78
+ }),
79
+ )
80
+
81
+ return sections.join("\n\n")
82
+ }
83
+
84
+ return {
85
+ tool: {
86
+ parent_session_messages: tool({
87
+ description:
88
+ "Fetch all messages from the parent session. " +
89
+ "Returns the full conversation with agent attribution " +
90
+ "and message content. Only works from subagent sessions " +
91
+ "(sessions with a parentID).",
92
+ args: {},
93
+ async execute(_args, context) {
94
+ const session = await client.session.get({
95
+ path: { id: context.sessionID },
96
+ })
97
+ const parent = session.data?.parentID
98
+ if (!parent) {
99
+ return `Error: Session ${context.sessionID} has no parent. This tool only works from subagent sessions.`
100
+ }
101
+
102
+ return formatSessionMessages(
103
+ parent,
104
+ `Session ${parent} has no messages.`,
105
+ )
106
+ },
107
+ }),
108
+ session_messages: tool({
109
+ description:
110
+ "Fetch all messages from a session by ID. " +
111
+ "Returns the full conversation with agent attribution " +
112
+ "and message content.",
113
+ args: {
114
+ sessionId: tool.schema.string().describe("The session ID to read"),
115
+ },
116
+ async execute(args) {
117
+ return formatSessionMessages(
118
+ args.sessionId,
119
+ `Session ${args.sessionId} has no messages.`,
120
+ )
121
+ },
122
+ }),
123
+ session_messages_batch: tool({
124
+ description:
125
+ "Fetch all messages from multiple sessions by ID. Returns the full conversations concatenated with session delimiters. Useful for reading multiple related sessions (e.g., TDD phase sessions from a dispatch log) in a single tool call.",
126
+ args: {
127
+ sessionIds: tool.schema
128
+ .array(tool.schema.string())
129
+ .describe("Array of session IDs to fetch messages from."),
130
+ },
131
+ async execute(args) {
132
+ return formatSessionMessagesBatch(args.sessionIds)
133
+ },
134
+ }),
135
+ },
136
+ }
137
+ }