opencode-wyvern 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/src/setup.js ADDED
@@ -0,0 +1,271 @@
1
+ import { section, pwshProfilePath, bashRcPath, configFilePath } from "./shell.js"
2
+ import { confirm, ask, checkbox, secret, closePrompts, c } from "./prompts.js"
3
+ import { loadConfig, saveConfig } from "./config.js"
4
+ import {
5
+ SECTIONS,
6
+ PLUGIN_CHOICES,
7
+ getSection,
8
+ defaultSections,
9
+ renderSection,
10
+ applyLocalSection,
11
+ applyRemoteSections,
12
+ } from "./sections.js"
13
+ import {
14
+ PROVIDER_META,
15
+ PROVIDER_ORDER,
16
+ defaultOmniRouteBase,
17
+ isPortValid,
18
+ ensureLocalKey,
19
+ ensureSshConfigAlias,
20
+ installKeyOnServer,
21
+ verifyConnection,
22
+ } from "./server.js"
23
+ import { installClient } from "./client.js"
24
+
25
+ const SAMPLE_ENTRY = { server: "remote-server", host: "server.example.com", user: "you", dir: "~" }
26
+ const REMOTE_SECTIONS = new Set(["server", "commands", "providers", "plugins", "claude-mem"])
27
+
28
+ /** Coordina (dove, user, porta) e alias solo per le sezioni che le richiedono. */
29
+ async function collectEntrySmart(prev, needs) {
30
+ const d = prev || {}
31
+ const out = {}
32
+ if (needs.host) {
33
+ out.host = await ask("IP o hostname del server remoto", {
34
+ hint: "es. server.example.com o 192.0.2.1 (solo esempio)",
35
+ defaultValue: d.host || "",
36
+ validate: (v) => v.length > 0,
37
+ })
38
+ out.user = await ask("Utente SSH sul server", {
39
+ hint: "es. root, ubuntu (invio = solo hostname)",
40
+ defaultValue: d.user || "",
41
+ })
42
+ out.port = await ask("Porta SSH (invio = 22)", {
43
+ defaultValue: d.port || "",
44
+ validate: (v) => isPortValid(v || ""),
45
+ })
46
+ }
47
+ if (needs.alias) {
48
+ out.server = await ask("Nome con cui i comandi oc-* chiamano il server (alias SSH)", {
49
+ hint: "qualsiasi nome, es. server-casa — finisce in ~/.ssh/config e in OC_SERVER",
50
+ defaultValue: d.server || "remote-server",
51
+ validate: (v) => /^[a-zA-Z0-9._-]+$/.test(v),
52
+ })
53
+ }
54
+ if (needs.dir) {
55
+ out.dir = await ask("Cartella remota di default (dir sessione)", {
56
+ hint: "es. ~ o ~/projects",
57
+ defaultValue: d.dir || "~",
58
+ })
59
+ }
60
+ return out
61
+ }
62
+
63
+ /** Raccoglie i provider attivi, gli endpoint e le chiavi (che restano solo sul server). */
64
+ async function collectProvidersCfg(prev) {
65
+ const choices = PROVIDER_ORDER.map((id) => ({
66
+ label: PROVIDER_META[id].name + (PROVIDER_META[id].key ? " (key in .env)" : ""),
67
+ value: id,
68
+ }))
69
+ const sel = await checkbox("Quali provider abilitare sul server?", choices)
70
+ const providers = Object.fromEntries(PROVIDER_ORDER.map((id) => [id, false]))
71
+ sel.forEach((s) => (providers[s.value] = true))
72
+
73
+ let omnirouteUrl = (prev && prev.omnirouteUrl) || ""
74
+ if (providers.omniroute) {
75
+ omnirouteUrl = await ask("URL gateway OmniRoute (senza /v1)", {
76
+ hint: "es. http://127.0.0.1:20128",
77
+ defaultValue: omnirouteUrl || defaultOmniRouteBase(),
78
+ validate: (v) => /^https?:\/\/\S+$/.test(v),
79
+ })
80
+ }
81
+
82
+ const envKeys = {}
83
+ for (const id of PROVIDER_ORDER) {
84
+ const key = PROVIDER_META[id].key
85
+ if (providers[id] && key) {
86
+ envKeys[key] = await secret(`API key ${PROVIDER_META[id].name} (${key}) — resta sul server`, { allowEmpty: true })
87
+ }
88
+ }
89
+ if (Object.values(envKeys).some(Boolean)) {
90
+ console.log(c.dim(" (le chiavi NON vengono salvate in locale; finiscono solo in ~/.config/opencode/.env sul server, chmod 600)"))
91
+ }
92
+
93
+ const models = Object.fromEntries(
94
+ PROVIDER_ORDER.filter((id) => providers[id]).map((id) => [id, (PROVIDER_META[id].defModels || []).join(", ")]),
95
+ )
96
+ return { providers, models, envKeys, omnirouteUrl }
97
+ }
98
+
99
+ export async function run(argv = []) {
100
+ console.log(c.bold(c.cyan("oc-setup — OpenCode Wyvern "))
101
+ + c.dim("(setup modulare: installi tutto, attivi quello che vuoi — nessun dato privato nel pacchetto)"))
102
+ section("Moduli")
103
+
104
+ const cfg = loadConfig()
105
+ const active = await checkbox("Quali sezioni vuoi attivare ora?", SECTIONS.map((s) => ({
106
+ label: s.label,
107
+ value: s.id,
108
+ })))
109
+ const activeIds = new Set(active.map((s) => s.value))
110
+
111
+ if (!activeIds.size) {
112
+ console.log(c.yellow(" nessun modulo attivo: niente da installare. Esci e riavvia con \`oc-setup\` per sceglierne almeno uno."))
113
+ closePrompts()
114
+ return
115
+ }
116
+
117
+ const needs = {
118
+ host: activeIds.has("ssh") || [...REMOTE_SECTIONS].some((id) => activeIds.has(id)),
119
+ alias: activeIds.has("ssh") || activeIds.has("client-pwsh") || activeIds.has("client-bash"),
120
+ dir: activeIds.has("client-pwsh") || activeIds.has("client-bash"),
121
+ }
122
+
123
+ section("Connessione")
124
+ console.log(c.dim(" solo i dati necessari ai moduli che hai scelto."))
125
+ const entry = await collectEntrySmart(cfg.entry || {}, needs)
126
+
127
+ let providers = cfg.providers || Object.fromEntries(PROVIDER_ORDER.map((id) => [id, false]))
128
+ let models = cfg.models || {}
129
+ let envKeys = {}
130
+ let omnirouteUrl = cfg.omnirouteUrl || ""
131
+ if (activeIds.has("providers")) {
132
+ const p = await collectProvidersCfg(cfg)
133
+ providers = p.providers
134
+ models = p.models
135
+ omnirouteUrl = p.omnirouteUrl
136
+ envKeys = p.envKeys
137
+ }
138
+
139
+ let plugins = cfg.plugins || []
140
+ if (activeIds.has("plugins")) {
141
+ const sel = await checkbox("Plugin opencode da includere nel config server?", PLUGIN_CHOICES.map((p) => ({ label: p.label, value: p.value })))
142
+ plugins = sel.map((s) => s.value)
143
+ }
144
+
145
+ let tuning = cfg.tuning !== false
146
+ if (activeIds.has("providers") || activeIds.has("plugins") || activeIds.has("claude-mem") || activeIds.has("server")) {
147
+ tuning = await confirm("Aggiungo al config tool_output + compaction (default robusti)?", cfg.tuning !== false)
148
+ }
149
+
150
+ const sections = Object.fromEntries(SECTIONS.map((s) => [s.id, activeIds.has(s.id)]))
151
+ saveConfig({ entry, sections, providers, models, plugins, omnirouteUrl, tuning, configuredAt: new Date().toISOString() })
152
+
153
+ section("Applicazione")
154
+ const anyClient = activeIds.has("client-pwsh") || activeIds.has("client-bash")
155
+ if (!activeIds.has("ssh") && anyClient && !entry.host) {
156
+ console.log(c.dim(` client senza host: oc-* userà l'alias \`${entry.server}\`. Assicurati che ~/.ssh/config lo definisca.`))
157
+ }
158
+
159
+ const { key } = ensureLocalKey()
160
+ console.log(c.dim(` chiave: ${key}`))
161
+
162
+ if (activeIds.has("ssh")) {
163
+ ensureSshConfigAlias(entry)
164
+ const doInstall = await confirm(`Installo la chiave pubblica su ${entry.user ? entry.user + "@" : ""}${entry.host} (password una volta)?`, true)
165
+ if (doInstall) {
166
+ try {
167
+ installKeyOnServer(entry)
168
+ } catch (err) {
169
+ console.log(c.yellow(` attenzione: ${err.message}`))
170
+ }
171
+ }
172
+ } else if (entry.host && (activeIds.has("client-pwsh") || activeIds.has("client-bash") || [...REMOTE_SECTIONS].some((id) => activeIds.has(id)))) {
173
+ ensureSshConfigAlias(entry)
174
+ }
175
+
176
+ const localClientTargets = []
177
+ for (const t of ["client-pwsh", "client-bash"]) {
178
+ if (activeIds.has(t)) {
179
+ await applyLocalSection(t, { entry })
180
+ localClientTargets.push(t)
181
+ }
182
+ }
183
+
184
+ if ([...REMOTE_SECTIONS].some((id) => activeIds.has(id))) {
185
+ const ok = verifyConnection(entry)
186
+ if (!ok) console.log(c.yellow(" server non raggiungibile ora: script pronto, esegui `oc-setup generate` quando torna"))
187
+ const saved = loadConfig()
188
+ applyRemoteSections(saved, { envKeys })
189
+ }
190
+
191
+ console.log(c.green("\nFatto."))
192
+ console.log(` config : ${configFilePath()} (solo dati non sensibili)`)
193
+ if (localClientTargets.length) {
194
+ console.log(` client : ${localClientTargets.map((t) => t === "client-pwsh" ? "PowerShell" : "bash").join(", ")} → ricarica profilo per attivare i comandi oc-*`)
195
+ }
196
+ closePrompts()
197
+ }
198
+
199
+ // ---------- subcomandi ----------
200
+
201
+ export async function cmdStatus() {
202
+ const cfg = loadConfig()
203
+ const e = cfg.entry
204
+ const providers = cfg.providers && Object.entries(cfg.providers).filter(([, v]) => v).map(([k]) => k)
205
+ console.log(c.bold(c.cyan("OpenCode Wyvern — stato")))
206
+ console.log(` config: ${configFilePath()}`)
207
+ console.log(` shell : PowerShell ${pwshProfilePath()} | bash ${bashRcPath()}`)
208
+ console.log(` entry : ${e ? `${e.user ? e.user + "@" : ""}${e.host || e.server}${e.port && e.port !== "22" ? ":" + e.port : ""} (alias: ${e.server || "-"}, dir: ${e.dir || "~"})` : "non configurata (esegui oc-setup)"}`)
209
+ if (providers?.length) console.log(` server: provider ${providers.join(", ")}${cfg.omnirouteUrl ? ` · omniroute ${cfg.omnirouteUrl}` : ""}${cfg.tuning ? " · tuning on" : ""}`)
210
+ console.log("")
211
+ console.log(" moduli:")
212
+ for (const s of SECTIONS) {
213
+ const active = cfg.sections?.[s.id]
214
+ console.log(` ${active ? c.green("●") : c.dim("○")} ${s.id.padEnd(12)} ${active ? c.dim(s.label) : c.dim(s.label + " (disattivo)")}`)
215
+ }
216
+ }
217
+
218
+ export async function cmdActivate(id) {
219
+ if (!getSection(id)) return fail(`sezione sconosciuta: ${id}`)
220
+ const cfg = loadConfig()
221
+ cfg.sections = { ...defaultSections(), ...(cfg.sections || {}), [id]: true }
222
+ saveConfig(cfg)
223
+ console.log(c.green(` sezione attivata: ${id}`))
224
+ if (getSection(id).kind === "remote") {
225
+ console.log(c.dim(` per applicarla sul server: oc-setup generate`))
226
+ } else if (id === "client-pwsh" || id === "client-bash") {
227
+ await installClient(cfg.entry || SAMPLE_ENTRY, { targets: [id === "client-bash" ? "bash" : "pwsh"] })
228
+ }
229
+ }
230
+
231
+ export async function cmdDeactivate(id) {
232
+ if (!getSection(id)) return fail(`sezione sconosciuta: ${id}`)
233
+ const cfg = loadConfig()
234
+ cfg.sections = { ...defaultSections(), ...(cfg.sections || {}), [id]: false }
235
+ saveConfig(cfg)
236
+ console.log(c.yellow(` sezione disattivata: ${id}`))
237
+ }
238
+
239
+ export async function cmdGenerate() {
240
+ const cfg = loadConfig()
241
+ if (!cfg.entry) return fail("nessuna config: esegui prima `oc-setup`")
242
+ const act = new Set(Object.entries(cfg.sections || {}).filter(([, v]) => v).map(([k]) => k))
243
+ for (const id of ["client-pwsh", "client-bash"]) {
244
+ if (act.has(id)) await applyLocalSection(id, { entry: cfg.entry })
245
+ }
246
+ if (act.has("ssh")) {
247
+ ensureSshConfigAlias(cfg.entry)
248
+ console.log(c.dim(" alias SSH assicurato."))
249
+ console.log(c.dim(" chiave: install via `oc-setup` se server raggiungibile."))
250
+ }
251
+ const remote = [...act].filter((id) => getSection(id)?.kind === "remote").length
252
+ if (remote) applyRemoteSections(cfg)
253
+ }
254
+
255
+ export async function cmdPrint(id) {
256
+ if (!getSection(id)) return fail(`sezione sconosciuta: ${id}`)
257
+ const cfg = loadConfig()
258
+ const rendered = renderSection(id, cfg.entry ? cfg : SAMPLE_ENTRY)
259
+ if (!rendered) return fail(`sezione non stampabile: ${id}`)
260
+ console.log(rendered.content)
261
+ }
262
+
263
+ function fail(msg) {
264
+ console.error(c.red(msg))
265
+ }
266
+
267
+ // mantiene compat con vecchio bin: per --print-client
268
+ export async function runPrintClient(kind) {
269
+ const id = kind === "bash" ? "client-bash" : "client-pwsh"
270
+ await cmdPrint(id)
271
+ }
package/src/shell.js ADDED
@@ -0,0 +1,102 @@
1
+ import { spawnSync } from "node:child_process"
2
+ import { homedir, hostname, platform } from "node:os"
3
+ import path from "node:path"
4
+ import fs from "node:fs"
5
+
6
+ export function isWindows() {
7
+ return platform() === "win32"
8
+ }
9
+
10
+ export function isBashShell() {
11
+ return !isWindows()
12
+ }
13
+
14
+ /** Profilo PowerShell corrente (AllHosts), stile Windows/macOS/Linux. */
15
+ export function pwshProfilePath() {
16
+ if (isWindows()) {
17
+ return path.join(homedir(), "Documents", "PowerShell", "profile.ps1")
18
+ }
19
+ // macOS/Linux con pwsh installato
20
+ return path.join(homedir(), ".config", "powershell", "profile.ps1")
21
+ }
22
+
23
+ export function bashRcPath() {
24
+ return path.join(homedir(), ".bashrc")
25
+ }
26
+
27
+ export function keyPaths() {
28
+ return {
29
+ key: path.join(homedir(), ".ssh", "id_ed25519"),
30
+ pub: path.join(homedir(), ".ssh", "id_ed25519.pub"),
31
+ }
32
+ }
33
+
34
+ export function sshConfigPath() {
35
+ return path.join(homedir(), ".ssh", "config")
36
+ }
37
+
38
+ export function configDir() {
39
+ return path.join(homedir(), ".config", "opencode-wyvern")
40
+ }
41
+
42
+ export function configFilePath() {
43
+ return path.join(configDir(), "config.json")
44
+ }
45
+
46
+ /** Costruisce lo string target SSH `[user@]host`. */
47
+ export function sshTarget({ user, host, port }) {
48
+ const u = user ? `${user}@` : ""
49
+ return port ? `${u}${host} -p ${port}` : `${u}${host}`
50
+ }
51
+
52
+ /**
53
+ * Esegue un comando sincrono catturando stdout/stderr.
54
+ * Con `stdio: true` il child eredita il terminale (utile per la password SSH).
55
+ */
56
+ export function run(cmd, args = [], { silent = false, stdio = false, timeout = 120000 } = {}) {
57
+ const res = spawnSync(cmd, args, {
58
+ stdio: stdio ? "inherit" : ["ignore", "pipe", "pipe"],
59
+ timeout,
60
+ encoding: "utf8",
61
+ })
62
+ const out = (res.stdout || "").toString().trim()
63
+ const err = (res.stderr || "").toString().trim()
64
+ const ok = res.status === 0
65
+ if (!silent && !stdio && (err || (!ok && !out))) {
66
+ if (err) process.stderr.write(err + "\n")
67
+ }
68
+ return { ok, status: res.status, stdout: out, stderr: err, signal: res.signal }
69
+ }
70
+
71
+ /** Trova un eseguibile nel PATH (Windows incluso). */
72
+ export function which(bin) {
73
+ const scan = isWindows() ? process.env.PATH.split(";") : process.env.PATH.split(":")
74
+ for (const dir of scan) {
75
+ try {
76
+ const candidates = isWindows() ? [bin, `${bin}.exe`, `${bin}.cmd`, `${bin}.ps1`] : [bin]
77
+ for (const c of candidates) {
78
+ const p = path.join(dir, c)
79
+ if (fs.existsSync(p)) return p
80
+ }
81
+ } catch {
82
+ /* ignora */
83
+ }
84
+ }
85
+ return null
86
+ }
87
+
88
+ export function currentUser() {
89
+ return process.env.USER || process.env.USERNAME || "user"
90
+ }
91
+
92
+ export function currentHost() {
93
+ return hostname() || "localhost"
94
+ }
95
+
96
+ /** Stampa una sezione intestazione nel flusso della CLI. */
97
+ export const section = (title) => {
98
+ const bar = "─".repeat(Math.min(56, process.stdout.columns ? process.stdout.columns - 2 : 56))
99
+ console.log(`\n\x1b[36m${bar}\x1b[0m`)
100
+ console.log(`\x1b[1m${title}\x1b[0m`)
101
+ console.log(`\x1b[36m${bar}\x1b[0m`)
102
+ }
@@ -0,0 +1,109 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import crypto from "node:crypto"
4
+ import { homedir } from "node:os"
5
+ import { spawnSync } from "node:child_process"
6
+ import { c, confirm, closePrompts } from "./prompts.js"
7
+ import { loadConfig } from "./config.js"
8
+ import { stripClientBlock, LEGACY_MARKERS } from "./client.js"
9
+ import { pwshProfilePath, bashRcPath, sshConfigPath, configDir } from "./shell.js"
10
+ import { sshTargetArgs } from "./server.js"
11
+
12
+ const CACHE_DIRS = ["opencode-wyvern", "opencode-remote"].map(
13
+ (name) => path.join(homedir(), ".cache", name),
14
+ )
15
+
16
+ /** Rimuove il blocco `Host <alias>` (fino al prossimo Host) e i blocchi legacy. */
17
+ function removeSshAliasBlock(content, alias) {
18
+ const lines = content.split(/\r?\n/)
19
+ const legacyIdx = lines.findIndex((l) => LEGACY_MARKERS.some((m) => l.includes(m)))
20
+ const keep = legacyIdx === -1 ? lines : lines.slice(0, legacyIdx)
21
+ const hostRe = new RegExp(`^Host\\s+${alias}(?:\\s.*)?$`, "i")
22
+ const out = []
23
+ let skip = false
24
+ for (const line of keep) {
25
+ const t = line.trim()
26
+ if (/^Host\s+\S+/.test(t)) skip = hostRe.test(t)
27
+ if (!skip) out.push(line)
28
+ }
29
+ return out.join("\n").replace(/[\r\n]+$/, "")
30
+ }
31
+
32
+ function rmDir(p, label) {
33
+ if (!fs.existsSync(p)) return false
34
+ fs.rmSync(p, { recursive: true, force: true })
35
+ return true
36
+ }
37
+
38
+ /**
39
+ * Disinstallazione completa: rimuove blocchi client (PowerShell + bash),
40
+ * config e cache locali, alias SSH dal config. La chiave SSH locale NON
41
+ * viene cancellata (potrebbe servire per altri accessi). La pulizia del
42
+ * server è opzionale e va confermata esplicitamente.
43
+ */
44
+ export async function runUninstall() {
45
+ const cfg = loadConfig()
46
+ const alias = (cfg.entry && cfg.entry.server) || "remote-server"
47
+
48
+ console.log(c.bold(c.cyan("oc-setup — disinstallazione completa")))
49
+ console.log(c.dim(" rimuovo: blocchi client (PowerShell+bash), config e cache locali, alias SSH."))
50
+ console.log(c.dim(" NON cancello la chiave ~/.ssh/id_ed25519 (potrebbe servire altrove)."))
51
+
52
+ const ok = await confirm("Procedo con la disinstallazione?", false)
53
+ if (!ok) {
54
+ console.log(c.yellow(" annullato."))
55
+ closePrompts()
56
+ return
57
+ }
58
+
59
+ let changed = false
60
+
61
+ for (const f of [pwshProfilePath(), bashRcPath()]) {
62
+ const r = stripClientBlock(f)
63
+ if (r.changed) {
64
+ changed = true
65
+ console.log(` - blocco client rimosso: ${r.filePath} (backup: ${r.backup})`)
66
+ }
67
+ }
68
+ if (!changed) console.log(c.dim(" nessun blocco client trovato nei profili."))
69
+
70
+ const sp = sshConfigPath()
71
+ if (fs.existsSync(sp)) {
72
+ const orig = fs.readFileSync(sp, "utf8")
73
+ const next = removeSshAliasBlock(orig, alias)
74
+ if (next !== orig) {
75
+ const backup = `${sp}.bak-${crypto.randomBytes(3).toString("hex")}`
76
+ fs.writeFileSync(backup, orig, "utf8")
77
+ fs.writeFileSync(sp, next ? next + "\n" : "", "utf8")
78
+ console.log(` - alias SSH '${alias}' rimosso da ${sp} (backup: ${backup})`)
79
+ } else {
80
+ console.log(c.dim(` nessun alias '${alias}' trovato in ${sp}.`))
81
+ }
82
+ }
83
+
84
+ for (const d of [configDir(), ...CACHE_DIRS]) {
85
+ if (rmDir(d)) console.log(` - rimosso: ${d}`)
86
+ }
87
+
88
+ if (cfg.entry && cfg.entry.host && cfg.entry.user) {
89
+ const clean = await confirm(
90
+ `Rimuovere anche la config opencode dal server (ssh ${cfg.entry.user}@${cfg.entry.host} → rm -rf ~/.config/opencode)?`,
91
+ false,
92
+ )
93
+ if (clean) {
94
+ console.log(c.cyan("[..] Rimuovo ~/.config/opencode sul server..."))
95
+ const res = spawnSync("ssh", [...sshTargetArgs(cfg.entry), "rm -rf ~/.config/opencode && echo DISINSTALL_OK"], {
96
+ encoding: "utf8",
97
+ stdio: ["ignore", "inherit", "inherit"],
98
+ timeout: 120000,
99
+ })
100
+ if (res.status === 0) console.log(c.green(" config opencode rimossa dal server."))
101
+ else console.log(c.yellow(" server non raggiungibile o rm fallito (da rifare a mano)."))
102
+ }
103
+ }
104
+
105
+ console.log(c.green("\nDisinstallazione completata."))
106
+ console.log(c.dim(` chiave SSH conservata: ${path.join(homedir(), ".ssh", "id_ed25519")}`))
107
+ console.log(c.dim(" Per una nuova installazione: oc-setup"))
108
+ closePrompts()
109
+ }
@@ -0,0 +1,17 @@
1
+ # OpenCode Wyvern - istruzioni di lavoro
2
+
3
+ Questo repository è raggiunto in remoto via opencode (SSH).
4
+
5
+ ## Regole di base
6
+
7
+ - `opencode` viene eseguito sul server; le sessioni sono avviate/riprese da terminali client.
8
+ - Mai committare o incollare chiavi, token o password: usa le variabili d'ambiente (`.env`) e i segreti forniti dal sistema.
9
+ - I plugin e i modelli configurati sono definiti in `~/.config/opencode/opencode.json` sul server.
10
+ - Per modifiche di configurazione, rileggere il file e riavviare la sessione; per i messaggi di memoria claude-mem la history è in `.claude-mem/`.
11
+
12
+ ## Flusso tipico
13
+
14
+ 1. Avvia/riprende una sessione con `oc-go` o `oc-resume`.
15
+ 2. Chiedi prima di modificare l'architettura: proponi la baseline (`/baseline-ui`).
16
+ 3. Usa git sul server (branch per feature, commit chiari).
17
+ 4. Quando la sessione termina, verifica con `oc-recap` quali sessioni sono ancora aperte.
@@ -0,0 +1,6 @@
1
+ // Wrapper di compatibilità per claude-mem.
2
+ // Re-esporta il plugin installato da `opcode-mem` con `export default`,
3
+ // così il loader di opencode lo carica correttamente anche quando insieme
4
+ // sono presenti altri plugin/auth provider. Nessun dato sensibile qui.
5
+
6
+ export { default } from "opcode-mem/dist/mem-plugin.js"