@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.
- package/LICENSE +21 -0
- package/README.md +397 -0
- package/bin/wevr.js +4 -0
- package/package.json +48 -0
- package/src/cli/commands/doctor.js +137 -0
- package/src/cli/commands/init.js +156 -0
- package/src/cli/commands/launch.js +122 -0
- package/src/cli/commands/theme.js +67 -0
- package/src/cli/commands/theme.test.js +28 -0
- package/src/cli/commands/uninstall.js +103 -0
- package/src/cli/commands/update.js +9 -0
- package/src/cli/index.js +63 -0
- package/src/cli/wizard/selectModelTier.js +40 -0
- package/src/core/agentPromptWriter.js +45 -0
- package/src/core/agentPromptWriter.test.js +56 -0
- package/src/core/backup.js +46 -0
- package/src/core/backup.test.js +51 -0
- package/src/core/commandsWriter.js +26 -0
- package/src/core/commandsWriter.test.js +29 -0
- package/src/core/configBuilder.js +32 -0
- package/src/core/configBuilder.test.js +93 -0
- package/src/core/configWriter.js +10 -0
- package/src/core/configWriter.test.js +26 -0
- package/src/core/identityHeader.js +8 -0
- package/src/core/identityHeader.test.js +15 -0
- package/src/core/paths.js +13 -0
- package/src/core/paths.test.js +33 -0
- package/src/core/pluginWriter.js +29 -0
- package/src/core/pluginWriter.test.js +41 -0
- package/src/core/skillsWriter.js +13 -0
- package/src/core/skillsWriter.test.js +30 -0
- package/src/core/themeWriter.js +26 -0
- package/src/core/themeWriter.test.js +29 -0
- package/src/core/tuiConfigWriter.js +22 -0
- package/src/core/tuiConfigWriter.test.js +38 -0
- package/src/core/version.js +8 -0
- package/src/core/versionCheck.js +44 -0
- package/src/core/versionCheck.test.js +34 -0
- package/src/plugins/README.md +57 -0
- package/src/plugins/wevr-flow.js +137 -0
- package/src/plugins/wevr-squeeze.js +3630 -0
- package/src/templates/agent-prompts/analyze.txt +43 -0
- package/src/templates/agent-prompts/builder.txt +10 -0
- package/src/templates/agent-prompts/compose.txt +45 -0
- package/src/templates/agent-prompts/debug.txt +43 -0
- package/src/templates/agent-prompts/explorer.txt +10 -0
- package/src/templates/agent-prompts/hierarchy.txt +95 -0
- package/src/templates/agent-prompts/reporter.txt +10 -0
- package/src/templates/agent-prompts/verifier.txt +10 -0
- package/src/templates/commands/squeeze-dashboard.md +5 -0
- package/src/templates/commands/squeeze-health.md +10 -0
- package/src/templates/commands/squeeze-quick.md +10 -0
- package/src/templates/model-defaults.json +59 -0
- package/src/templates/opencode.config.json +243 -0
- package/src/templates/skills/brooks-lint-rca/SKILL.md +48 -0
- package/src/templates/skills/codebase-fact-finding/SKILL.md +39 -0
- package/src/templates/skills/diff-review/SKILL.md +42 -0
- package/src/templates/skills/general-coding/SKILL.md +43 -0
- package/src/templates/skills/minimal-fixing/SKILL.md +25 -0
- package/src/templates/skills/plan-checking/SKILL.md +33 -0
- package/src/templates/skills/ponytail-patching/SKILL.md +20 -0
- package/src/templates/skills/prd-formatting/SKILL.md +45 -0
- package/src/templates/skills/refactoring-patterns/SKILL.md +37 -0
- package/src/templates/skills/security-auditing/SKILL.md +35 -0
- package/src/templates/skills/security-remediation/SKILL.md +37 -0
- package/src/templates/skills/summary-reporting/SKILL.md +83 -0
- package/src/templates/skills/test-assurance/SKILL.md +44 -0
- package/src/templates/skills/test-mocking-strategy/SKILL.md +18 -0
- package/src/templates/skills/ui-design-audit/SKILL.md +23 -0
- package/src/templates/skills/ui-design-system/SKILL.md +37 -0
- package/src/templates/skills/wstg-recon/SKILL.md +33 -0
- package/src/templates/themes/wevr-colorful.json +241 -0
- package/src/templates/themes/wevr-dark.json +177 -0
- 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
|
+
}
|