opencode-dir 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/README.md +44 -0
- package/index.ts +109 -0
- package/lib.ts +319 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# opencode-dir
|
|
2
|
+
|
|
3
|
+
Directory operations for [opencode](https://opencode.ai) sessions.
|
|
4
|
+
|
|
5
|
+
When working across monorepos or multiple repositories, sessions get stuck in the directory they were started in. This plugin adds `/cd` and `/mv` commands to move sessions between directories.
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
### `/cd <path>`
|
|
10
|
+
|
|
11
|
+
Change the session's working directory. Updates the session metadata without rewriting message history. Useful when you want the session to continue in a new location but don't need old file references updated.
|
|
12
|
+
|
|
13
|
+
### `/mv <path>`
|
|
14
|
+
|
|
15
|
+
Move the session to a new directory. Updates the session metadata **and** rewrites `path.cwd` and `path.root` in all assistant messages to point to the new location. Use this when relocating a session and its full context to a different repo.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
Add to your `opencode.json`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"plugin": ["opencode-dir"]
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
- The **target** directory must be inside a git repository. Moving to a non-git directory is not supported because opencode groups all non-git sessions under a single `"global"` project, making reliable moves impossible.
|
|
30
|
+
- Sessions started in non-git directories can be moved **into** a git repo — this is a valid way to "adopt" a global session into a proper project.
|
|
31
|
+
|
|
32
|
+
## How it works
|
|
33
|
+
|
|
34
|
+
1. Resolves the target directory and computes its project ID (the repo's initial commit hash, matching opencode's own logic)
|
|
35
|
+
2. Updates the session's `directory` and `project_id` in the database
|
|
36
|
+
3. For `/mv`, rewrites `path.cwd` and `path.root` in all assistant messages from the old directory to the new one
|
|
37
|
+
4. Intercepts subsequent tool calls (`bash`, `read`, `write`, `edit`, `glob`, `grep`) and rewrites file paths from the old directory to the new one — tools operate in the new directory immediately without a restart
|
|
38
|
+
5. Writes an `external_directory` permission rule on the session so tools can access the new directory without prompts
|
|
39
|
+
|
|
40
|
+
For a fully clean environment, restart opencode in the target directory after the move.
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { type Plugin } from "@opencode-ai/plugin"
|
|
2
|
+
import { mkdirSync, appendFileSync } from "fs"
|
|
3
|
+
import {
|
|
4
|
+
type Override,
|
|
5
|
+
type ExecResult,
|
|
6
|
+
loadOverrides,
|
|
7
|
+
persistOverrides,
|
|
8
|
+
execMove,
|
|
9
|
+
rewritePath,
|
|
10
|
+
PATH_TOOLS,
|
|
11
|
+
} from "./lib"
|
|
12
|
+
|
|
13
|
+
const STATE_DIR = `${process.env.XDG_DATA_HOME || process.env.HOME + "/.local/share"}/opencode`
|
|
14
|
+
const LOG_FILE = `${STATE_DIR}/opencode-dir-debug.log`
|
|
15
|
+
const OVERRIDES_FILE = `${STATE_DIR}/opencode-dir-overrides.json`
|
|
16
|
+
const DEBUG = !!process.env.OPENCODE_DIR_DEBUG
|
|
17
|
+
|
|
18
|
+
function log(...args: unknown[]) {
|
|
19
|
+
if (!DEBUG) return
|
|
20
|
+
const ts = new Date().toISOString()
|
|
21
|
+
const line = args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" ")
|
|
22
|
+
appendFileSync(LOG_FILE, `[${ts}] ${line}\n`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const dirOverrides: Map<string, Override> = loadOverrides(OVERRIDES_FILE)
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* opencode-dir plugin — adds `/cd` and `/mv` commands for moving sessions
|
|
29
|
+
* between directories at runtime.
|
|
30
|
+
*
|
|
31
|
+
* `/cd <path>` changes where tools operate without touching message history.
|
|
32
|
+
* `/mv <path>` does the same and rewrites historical paths in messages.
|
|
33
|
+
*/
|
|
34
|
+
export const OpencodeDir: Plugin = async ({ client }) => {
|
|
35
|
+
mkdirSync(STATE_DIR, { recursive: true })
|
|
36
|
+
log("plugin loaded", { overridesRecovered: dirOverrides.size })
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
"command.execute.before": async (input, output) => {
|
|
40
|
+
log("command.execute.before", { command: input.command, sessionID: input.sessionID })
|
|
41
|
+
if (input.command !== "cd" && input.command !== "mv") return
|
|
42
|
+
|
|
43
|
+
const targetPath = input.arguments.trim()
|
|
44
|
+
if (!targetPath) {
|
|
45
|
+
output.parts.splice(0)
|
|
46
|
+
output.parts.push({ type: "text", text: `Usage: /${input.command} <path>` })
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let exec: ExecResult
|
|
51
|
+
try {
|
|
52
|
+
exec = execMove(input.sessionID, targetPath, input.command === "mv")
|
|
53
|
+
} catch (e: unknown) {
|
|
54
|
+
exec = { result: `Error: ${e instanceof Error ? e.message : String(e)}` }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
output.parts.splice(0)
|
|
58
|
+
output.parts.push({ type: "text", text: exec.result })
|
|
59
|
+
|
|
60
|
+
if (exec.oldDir && exec.newDir) {
|
|
61
|
+
log("storing override", { sessionID: input.sessionID, oldDir: exec.oldDir, newDir: exec.newDir })
|
|
62
|
+
dirOverrides.set(input.sessionID, { oldDir: exec.oldDir, newDir: exec.newDir })
|
|
63
|
+
persistOverrides(OVERRIDES_FILE, dirOverrides)
|
|
64
|
+
|
|
65
|
+
await client.tui.showToast({
|
|
66
|
+
body: {
|
|
67
|
+
title: "Session directory changed",
|
|
68
|
+
message: `Now operating in ${exec.newDir}.\nRestart opencode there for a clean slate.`,
|
|
69
|
+
variant: "info",
|
|
70
|
+
duration: 8000,
|
|
71
|
+
},
|
|
72
|
+
}).catch(() => {})
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
"tool.execute.before": async (input, output) => {
|
|
77
|
+
const override = dirOverrides.get(input.sessionID)
|
|
78
|
+
if (!override) return
|
|
79
|
+
log("tool.execute.before", { tool: input.tool, sessionID: input.sessionID })
|
|
80
|
+
|
|
81
|
+
const { oldDir, newDir } = override
|
|
82
|
+
const pathKeys = PATH_TOOLS[input.tool]
|
|
83
|
+
if (pathKeys) {
|
|
84
|
+
for (const key of pathKeys) {
|
|
85
|
+
if (typeof output.args[key] === "string") {
|
|
86
|
+
output.args[key] = rewritePath(output.args[key], oldDir, newDir)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (input.tool === "bash") {
|
|
92
|
+
if (!output.args.workdir) output.args.workdir = newDir
|
|
93
|
+
if (typeof output.args.command === "string") {
|
|
94
|
+
output.args.command = output.args.command.replaceAll(oldDir, newDir)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
"shell.env": async (input, output) => {
|
|
100
|
+
const override = dirOverrides.get(input.sessionID ?? "")
|
|
101
|
+
if (!override) return
|
|
102
|
+
|
|
103
|
+
const { oldDir, newDir } = override
|
|
104
|
+
if (input.cwd === oldDir || input.cwd.startsWith(oldDir + "/")) {
|
|
105
|
+
output.env.PWD = rewritePath(input.cwd, oldDir, newDir)
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
}
|
package/lib.ts
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite"
|
|
2
|
+
import { resolve } from "path"
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "fs"
|
|
4
|
+
import { execSync } from "child_process"
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Overrides
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export interface Override {
|
|
11
|
+
oldDir: string
|
|
12
|
+
newDir: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Loads session directory overrides from a JSON file on disk. */
|
|
16
|
+
export function loadOverrides(path: string): Map<string, Override> {
|
|
17
|
+
try {
|
|
18
|
+
const entries = JSON.parse(readFileSync(path, "utf-8")) as [string, Override][]
|
|
19
|
+
return new Map(entries)
|
|
20
|
+
} catch {
|
|
21
|
+
return new Map()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Persists session directory overrides to disk (owner-only). */
|
|
26
|
+
export function persistOverrides(path: string, map: Map<string, Override>) {
|
|
27
|
+
writeFileSync(path, JSON.stringify([...map.entries()]), { mode: 0o600 })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Git
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolves the initial commit hash for a git repository, matching
|
|
36
|
+
* opencode's `Project.fromDirectory` logic:
|
|
37
|
+
* `git rev-list --max-parents=0 --all`, split/filter/sort, take first.
|
|
38
|
+
*/
|
|
39
|
+
export function getInitialCommit(dir: string): string | null {
|
|
40
|
+
try {
|
|
41
|
+
const output = execSync("git rev-list --max-parents=0 --all", {
|
|
42
|
+
cwd: dir,
|
|
43
|
+
encoding: "utf-8",
|
|
44
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
45
|
+
})
|
|
46
|
+
return output.split("\n").filter(Boolean).map((x) => x.trim()).sort()[0] ?? null
|
|
47
|
+
} catch {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Database
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/** Creates the minimal schema required by the plugin in a fresh database. */
|
|
57
|
+
export function createSchema(db: Database) {
|
|
58
|
+
db.exec(`
|
|
59
|
+
CREATE TABLE IF NOT EXISTS project (
|
|
60
|
+
id TEXT PRIMARY KEY,
|
|
61
|
+
worktree TEXT NOT NULL,
|
|
62
|
+
vcs TEXT NOT NULL,
|
|
63
|
+
name TEXT,
|
|
64
|
+
icon_url TEXT,
|
|
65
|
+
icon_color TEXT,
|
|
66
|
+
time_created INTEGER NOT NULL,
|
|
67
|
+
time_updated INTEGER NOT NULL,
|
|
68
|
+
time_initialized INTEGER,
|
|
69
|
+
sandboxes TEXT NOT NULL DEFAULT '[]',
|
|
70
|
+
commands TEXT
|
|
71
|
+
);
|
|
72
|
+
CREATE TABLE IF NOT EXISTS session (
|
|
73
|
+
id TEXT PRIMARY KEY,
|
|
74
|
+
project_id TEXT NOT NULL REFERENCES project(id) ON DELETE CASCADE,
|
|
75
|
+
workspace_id TEXT,
|
|
76
|
+
parent_id TEXT,
|
|
77
|
+
slug TEXT NOT NULL,
|
|
78
|
+
directory TEXT NOT NULL,
|
|
79
|
+
title TEXT NOT NULL,
|
|
80
|
+
version TEXT NOT NULL,
|
|
81
|
+
share_url TEXT,
|
|
82
|
+
summary_additions INTEGER,
|
|
83
|
+
summary_deletions INTEGER,
|
|
84
|
+
summary_files INTEGER,
|
|
85
|
+
summary_diffs TEXT,
|
|
86
|
+
revert TEXT,
|
|
87
|
+
permission TEXT,
|
|
88
|
+
time_created INTEGER NOT NULL,
|
|
89
|
+
time_updated INTEGER NOT NULL,
|
|
90
|
+
time_compacting INTEGER,
|
|
91
|
+
time_archived INTEGER
|
|
92
|
+
);
|
|
93
|
+
CREATE TABLE IF NOT EXISTS message (
|
|
94
|
+
id TEXT PRIMARY KEY,
|
|
95
|
+
session_id TEXT NOT NULL REFERENCES session(id) ON DELETE CASCADE,
|
|
96
|
+
time_created INTEGER NOT NULL,
|
|
97
|
+
time_updated INTEGER NOT NULL,
|
|
98
|
+
data TEXT NOT NULL
|
|
99
|
+
);
|
|
100
|
+
`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Creates a project row if one does not already exist. */
|
|
104
|
+
export function ensureProject(db: Database, projectId: string, worktree: string) {
|
|
105
|
+
if (db.query("SELECT id FROM project WHERE id = ?").get(projectId)) return
|
|
106
|
+
|
|
107
|
+
const now = Date.now()
|
|
108
|
+
db.run(
|
|
109
|
+
`INSERT INTO project (id, worktree, vcs, name, icon_url, icon_color, time_created, time_updated, time_initialized, sandboxes, commands)
|
|
110
|
+
VALUES (?, ?, 'git', NULL, NULL, 'blue', ?, ?, NULL, '[]', NULL)`,
|
|
111
|
+
[projectId, worktree, now, now],
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Updates a session's directory, project, and permission in one statement.
|
|
117
|
+
*
|
|
118
|
+
* Writes an `external_directory` allow rule to `session.permission` so
|
|
119
|
+
* `PermissionNext` auto-allows tool access to the target tree.
|
|
120
|
+
* `prompt()` loads the session AFTER `command.execute.before` fires,
|
|
121
|
+
* so the rule is available when tools run.
|
|
122
|
+
*/
|
|
123
|
+
export function updateSession(db: Database, sessionId: string, newDir: string, newProjectId: string): number {
|
|
124
|
+
const permission = JSON.stringify([
|
|
125
|
+
{ permission: "external_directory", pattern: newDir + "/*", action: "allow" },
|
|
126
|
+
])
|
|
127
|
+
return db.run(
|
|
128
|
+
`UPDATE session SET directory = ?, project_id = ?, permission = ?, time_updated = ? WHERE id = ?`,
|
|
129
|
+
[newDir, newProjectId, permission, Date.now(), sessionId],
|
|
130
|
+
).changes
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Rewrites `path.cwd` and `path.root` in message data from `oldDir` to
|
|
135
|
+
* `newDir`. Runs inside a transaction for atomicity.
|
|
136
|
+
*/
|
|
137
|
+
export function rewriteMessages(
|
|
138
|
+
db: Database,
|
|
139
|
+
sessionId: string,
|
|
140
|
+
oldDir: string,
|
|
141
|
+
newDir: string,
|
|
142
|
+
): { total: number; rewritten: number } {
|
|
143
|
+
const messages = db
|
|
144
|
+
.query("SELECT id, data FROM message WHERE session_id = ?")
|
|
145
|
+
.all(sessionId) as { id: string; data: string }[]
|
|
146
|
+
|
|
147
|
+
let rewritten = 0
|
|
148
|
+
const update = db.prepare("UPDATE message SET data = ? WHERE id = ?")
|
|
149
|
+
const tx = db.transaction(() => {
|
|
150
|
+
for (const msg of messages) {
|
|
151
|
+
const data = JSON.parse(msg.data)
|
|
152
|
+
let changed = false
|
|
153
|
+
|
|
154
|
+
if (data.path) {
|
|
155
|
+
if (data.path.cwd === oldDir) {
|
|
156
|
+
data.path.cwd = newDir
|
|
157
|
+
changed = true
|
|
158
|
+
}
|
|
159
|
+
if (data.path.root === oldDir) {
|
|
160
|
+
data.path.root = newDir
|
|
161
|
+
changed = true
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (changed) {
|
|
166
|
+
update.run(JSON.stringify(data), msg.id)
|
|
167
|
+
rewritten++
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
tx()
|
|
172
|
+
|
|
173
|
+
return { total: messages.length, rewritten }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Resolves a user-provided path to an absolute directory and its git
|
|
178
|
+
* project ID. Throws if the path does not exist or is not inside a
|
|
179
|
+
* git repository.
|
|
180
|
+
*/
|
|
181
|
+
export function resolveTarget(targetPath: string): { dir: string; projectId: string } {
|
|
182
|
+
const dir = resolve(targetPath.replace(/^~/, process.env.HOME || "/root"))
|
|
183
|
+
|
|
184
|
+
if (!existsSync(dir)) {
|
|
185
|
+
throw new Error(`Directory does not exist: ${dir}`)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const projectId = getInitialCommit(dir)
|
|
189
|
+
if (!projectId) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`Target directory is not inside a git repository.\n` +
|
|
192
|
+
`/cd and /mv only work with git repos — non-git directories ` +
|
|
193
|
+
`share a single "global" project in opencode, making session moves unreliable.`,
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { dir, projectId }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Reads session directory and project ID from the database. */
|
|
201
|
+
export function getSessionInfo(
|
|
202
|
+
db: Database,
|
|
203
|
+
sessionId: string,
|
|
204
|
+
): { directory: string; projectId: string } | null {
|
|
205
|
+
const row = db
|
|
206
|
+
.query("SELECT directory, project_id FROM session WHERE id = ?")
|
|
207
|
+
.get(sessionId) as { directory: string; project_id: string } | null
|
|
208
|
+
if (!row) return null
|
|
209
|
+
return { directory: row.directory, projectId: row.project_id }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Scans early assistant messages for `path.cwd` to determine the
|
|
214
|
+
* directory the session was originally operating in.
|
|
215
|
+
*/
|
|
216
|
+
export function getCurrentDirectory(db: Database, sessionId: string): string | null {
|
|
217
|
+
const rows = db
|
|
218
|
+
.query("SELECT data FROM message WHERE session_id = ? ORDER BY rowid ASC LIMIT 10")
|
|
219
|
+
.all(sessionId) as { data: string }[]
|
|
220
|
+
|
|
221
|
+
for (const row of rows) {
|
|
222
|
+
try {
|
|
223
|
+
const data = JSON.parse(row.data)
|
|
224
|
+
if (data.path?.cwd) return data.path.cwd
|
|
225
|
+
} catch {
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Core operation
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
export interface ExecResult {
|
|
238
|
+
result: string
|
|
239
|
+
oldDir?: string
|
|
240
|
+
newDir?: string
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Moves a session to a new directory.
|
|
245
|
+
*
|
|
246
|
+
* @param rewrite - When true (`/mv`), rewrites message history paths.
|
|
247
|
+
* When false (`/cd`), leaves history intact.
|
|
248
|
+
* @param db - Optional database instance (uses default path if omitted).
|
|
249
|
+
*/
|
|
250
|
+
export function execMove(
|
|
251
|
+
sessionId: string,
|
|
252
|
+
targetPath: string,
|
|
253
|
+
rewrite: boolean,
|
|
254
|
+
db?: Database,
|
|
255
|
+
): ExecResult {
|
|
256
|
+
const { dir, projectId } = resolveTarget(targetPath)
|
|
257
|
+
const owned = !db
|
|
258
|
+
if (!db) {
|
|
259
|
+
const stateDir = `${process.env.XDG_DATA_HOME || process.env.HOME + "/.local/share"}/opencode`
|
|
260
|
+
db = new Database(`${stateDir}/opencode.db`)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const session = getSessionInfo(db, sessionId)
|
|
265
|
+
if (!session) {
|
|
266
|
+
return { result: `Error: session ${sessionId} not found in database.` }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const currentDir = getCurrentDirectory(db, sessionId) ?? session.directory
|
|
270
|
+
if (dir === currentDir) {
|
|
271
|
+
return { result: `Already in ${dir} — no change needed.` }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
ensureProject(db, projectId, dir)
|
|
275
|
+
const changes = updateSession(db, sessionId, dir, projectId)
|
|
276
|
+
if (changes === 0) {
|
|
277
|
+
return { result: `Error: session ${sessionId} not found in database.` }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const lines = rewrite
|
|
281
|
+
? (() => {
|
|
282
|
+
const { total, rewritten } = rewriteMessages(db!, sessionId, currentDir, dir)
|
|
283
|
+
return [
|
|
284
|
+
`Session moved: ${currentDir} -> ${dir}`,
|
|
285
|
+
`Project: ${projectId}`,
|
|
286
|
+
`Messages: ${rewritten}/${total} rewritten`,
|
|
287
|
+
]
|
|
288
|
+
})()
|
|
289
|
+
: [`Session directory changed: ${currentDir} -> ${dir}`, `Project: ${projectId}`]
|
|
290
|
+
|
|
291
|
+
lines.push("", `Tools will now operate in ${dir} for this session.`)
|
|
292
|
+
lines.push(`Restart opencode in the new directory for a clean slate.`)
|
|
293
|
+
|
|
294
|
+
return { oldDir: currentDir, newDir: dir, result: lines.join("\n") }
|
|
295
|
+
} finally {
|
|
296
|
+
if (owned) db.close()
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// Path rewriting
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
/** Replaces `oldDir` prefix with `newDir` in a file path. */
|
|
305
|
+
export function rewritePath(filePath: string, oldDir: string, newDir: string): string {
|
|
306
|
+
if (filePath === oldDir) return newDir
|
|
307
|
+
if (filePath.startsWith(oldDir + "/")) return newDir + filePath.slice(oldDir.length)
|
|
308
|
+
return filePath
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Map of tool IDs to arg keys that carry file paths. */
|
|
312
|
+
export const PATH_TOOLS: Record<string, string[]> = {
|
|
313
|
+
read: ["filePath"],
|
|
314
|
+
write: ["filePath"],
|
|
315
|
+
edit: ["filePath"],
|
|
316
|
+
glob: ["path"],
|
|
317
|
+
grep: ["path"],
|
|
318
|
+
bash: ["workdir"],
|
|
319
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-dir",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Directory operations for opencode sessions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "index.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"index.ts",
|
|
10
|
+
"lib.ts"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "bun test"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"opencode",
|
|
17
|
+
"opencode-plugin",
|
|
18
|
+
"cd",
|
|
19
|
+
"mv",
|
|
20
|
+
"directory",
|
|
21
|
+
"monorepo"
|
|
22
|
+
],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/adiled/opencode-dir.git"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/adiled/opencode-dir#readme",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/adiled/opencode-dir/issues"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@opencode-ai/plugin": ">=1.2.0"
|
|
36
|
+
}
|
|
37
|
+
}
|