opencode-add-dir 1.6.0 → 1.7.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/bin/setup.mjs CHANGED
@@ -4,98 +4,55 @@ import { join } from "path"
4
4
  import { homedir } from "os"
5
5
 
6
6
  const PKG = "opencode-add-dir"
7
- const args = process.argv.slice(2)
8
- const isRemove = args.includes("--remove")
7
+ const isRemove = process.argv.includes("--remove")
8
+ const dir = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode")
9
9
 
10
- function configDir() {
11
- if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, "opencode")
12
- return join(homedir(), ".config", "opencode")
13
- }
14
-
15
- function stripJsonComments(text) {
16
- let result = ""
17
- let inString = false
18
- let escape = false
19
- for (let i = 0; i < text.length; i++) {
20
- const ch = text[i]
21
- if (escape) { result += ch; escape = false; continue }
22
- if (ch === "\\" && inString) { result += ch; escape = true; continue }
23
- if (ch === '"') { inString = !inString; result += ch; continue }
24
- if (inString) { result += ch; continue }
25
- if (ch === "/" && text[i + 1] === "/") { while (i < text.length && text[i] !== "\n") i++; continue }
26
- if (ch === "/" && text[i + 1] === "*") { i += 2; while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++; i++; continue }
27
- result += ch
28
- }
29
- return result
30
- }
31
-
32
- function findConfigFile(dir, baseName) {
10
+ function findFile(base) {
33
11
  for (const ext of [".jsonc", ".json"]) {
34
- const p = join(dir, baseName + ext)
12
+ const p = join(dir, base + ext)
35
13
  if (existsSync(p)) return p
36
14
  }
37
- return join(dir, baseName + ".json")
15
+ return join(dir, base + ".json")
38
16
  }
39
17
 
40
- function readConfig(filePath) {
41
- if (!existsSync(filePath)) return {}
42
- return JSON.parse(stripJsonComments(readFileSync(filePath, "utf-8")))
18
+ function readConfig(path) {
19
+ if (!existsSync(path)) return {}
20
+ return JSON.parse(readFileSync(path, "utf-8").replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""))
43
21
  }
44
22
 
45
23
  function hasPlugin(plugins) {
46
- return (plugins || []).some((p) => {
47
- const name = Array.isArray(p) ? p[0] : p
48
- return name === PKG || name.startsWith(PKG + "@")
49
- })
50
- }
51
-
52
- function withoutPlugin(plugins) {
53
- return (plugins || []).filter((p) => {
54
- const name = Array.isArray(p) ? p[0] : p
55
- return name !== PKG && !name.startsWith(PKG + "@")
24
+ return plugins.some((p) => {
25
+ const n = Array.isArray(p) ? p[0] : p
26
+ return n === PKG || n.startsWith(PKG + "@")
56
27
  })
57
28
  }
58
29
 
59
- function patchConfig(filePath, config, schemaUrl) {
60
- config.plugin = config.plugin || []
30
+ function patch(path, schema) {
31
+ const config = readConfig(path)
32
+ config.plugin ??= []
61
33
 
62
34
  if (isRemove) {
63
35
  if (!hasPlugin(config.plugin)) return false
64
- config.plugin = withoutPlugin(config.plugin)
65
- writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n")
66
- return true
36
+ config.plugin = config.plugin.filter((p) => {
37
+ const n = Array.isArray(p) ? p[0] : p
38
+ return n !== PKG && !n.startsWith(PKG + "@")
39
+ })
40
+ } else {
41
+ if (hasPlugin(config.plugin)) return false
42
+ config.plugin.push(PKG)
43
+ if (schema && !config.$schema) config.$schema = schema
67
44
  }
68
45
 
69
- if (hasPlugin(config.plugin)) return false
70
- config.plugin.push(PKG)
71
- if (schemaUrl && !config.$schema) config.$schema = schemaUrl
72
- writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n")
46
+ writeFileSync(path, JSON.stringify(config, null, 2) + "\n")
73
47
  return true
74
48
  }
75
49
 
76
- function run() {
77
- const dir = configDir()
78
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
50
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
79
51
 
80
- const serverPath = findConfigFile(dir, "opencode")
81
- const tuiPath = findConfigFile(dir, "tui")
82
-
83
- const serverConfig = readConfig(serverPath)
84
- const tuiConfig = readConfig(tuiPath)
85
-
86
- const verb = isRemove ? "Removed" : "Added"
87
- const serverChanged = patchConfig(serverPath, serverConfig, "https://opencode.ai/config.json")
88
- const tuiChanged = patchConfig(tuiPath, tuiConfig)
89
-
90
- if (serverChanged) console.log(`${verb} ${PKG} in ${serverPath}`)
91
- else console.log(`${PKG} already ${isRemove ? "absent from" : "in"} ${serverPath}`)
92
-
93
- if (tuiChanged) console.log(`${verb} ${PKG} in ${tuiPath}`)
94
- else console.log(`${PKG} already ${isRemove ? "absent from" : "in"} ${tuiPath}`)
95
-
96
- if (serverChanged || tuiChanged) {
97
- console.log("Restart OpenCode to activate the plugin.")
98
- }
52
+ for (const [label, path, schema] of [
53
+ ["server", findFile("opencode"), "https://opencode.ai/config.json"],
54
+ ["tui", findFile("tui")],
55
+ ]) {
56
+ if (patch(path, schema)) console.log(`${isRemove ? "Removed from" : "Added to"} ${label}: ${path}`)
57
+ else console.log(`${label}: already ${isRemove ? "absent" : "configured"}`)
99
58
  }
100
-
101
- run()
package/dist/index.js CHANGED
@@ -1,232 +1,7 @@
1
- // src/state.ts
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from "fs";
3
- import { join } from "path";
4
- function stateDir() {
5
- return join(process.env["XDG_DATA_HOME"] || join(process.env["HOME"] || "~", ".local", "share"), "opencode", "add-dir");
6
- }
7
- var dirsFile = () => join(stateDir(), "directories.json");
8
- function expandHome(p) {
9
- return p.startsWith("~/") ? (process.env["HOME"] || "~") + p.slice(1) : p;
10
- }
11
- function loadDirs() {
12
- const dirs = new Map;
13
- const file = dirsFile();
14
- if (!existsSync(file))
15
- return dirs;
16
- try {
17
- for (const p of JSON.parse(readFileSync(file, "utf-8")))
18
- dirs.set(p, { path: p, persist: true });
19
- } catch {}
20
- return dirs;
21
- }
22
- var cachedDirs;
23
- var cachedMtime = 0;
24
- function freshDirs() {
25
- const file = dirsFile();
26
- try {
27
- const mtime = statSync(file).mtimeMs;
28
- if (cachedDirs && mtime === cachedMtime)
29
- return cachedDirs;
30
- cachedMtime = mtime;
31
- cachedDirs = loadDirs();
32
- return cachedDirs;
33
- } catch {
34
- if (!cachedDirs)
35
- cachedDirs = new Map;
36
- return cachedDirs;
37
- }
38
- }
39
- function isChildOf(parent, child) {
40
- return child === parent || child.startsWith(parent + "/");
41
- }
42
- function matchesDirs(dirs, filepath) {
43
- for (const entry of dirs.values()) {
44
- if (isChildOf(entry.path, filepath))
45
- return true;
46
- }
47
- return false;
48
- }
49
- var PKG = "opencode-add-dir";
50
- function stripJsonComments(text) {
51
- let result = "";
52
- let inString = false;
53
- let escape = false;
54
- for (let i = 0;i < text.length; i++) {
55
- const ch = text[i];
56
- if (escape) {
57
- result += ch;
58
- escape = false;
59
- continue;
60
- }
61
- if (ch === "\\" && inString) {
62
- result += ch;
63
- escape = true;
64
- continue;
65
- }
66
- if (ch === '"') {
67
- inString = !inString;
68
- result += ch;
69
- continue;
70
- }
71
- if (inString) {
72
- result += ch;
73
- continue;
74
- }
75
- if (ch === "/" && text[i + 1] === "/") {
76
- while (i < text.length && text[i] !== `
77
- `)
78
- i++;
79
- continue;
80
- }
81
- if (ch === "/" && text[i + 1] === "*") {
82
- i += 2;
83
- while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
84
- i++;
85
- i++;
86
- continue;
87
- }
88
- result += ch;
89
- }
90
- return result;
91
- }
92
- function configDir() {
93
- return join(process.env["XDG_CONFIG_HOME"] || join(process.env["HOME"] || "~", ".config"), "opencode");
94
- }
95
- function findTuiConfig() {
96
- const dir = configDir();
97
- for (const name of ["tui.jsonc", "tui.json"]) {
98
- const p = join(dir, name);
99
- if (existsSync(p))
100
- return p;
101
- }
102
- return join(dir, "tui.json");
103
- }
104
- function ensureTuiConfig() {
105
- try {
106
- const dir = configDir();
107
- if (!existsSync(dir))
108
- mkdirSync(dir, { recursive: true });
109
- const filePath = findTuiConfig();
110
- let config = {};
111
- if (existsSync(filePath)) {
112
- config = JSON.parse(stripJsonComments(readFileSync(filePath, "utf-8")));
113
- }
114
- const plugins = config.plugin ?? [];
115
- const hasEntry = plugins.some((p) => {
116
- const name = Array.isArray(p) ? p[0] : p;
117
- return name === PKG || typeof name === "string" && name.startsWith(PKG + "@");
118
- });
119
- if (hasEntry)
120
- return;
121
- config.plugin = [...plugins, PKG];
122
- writeFileSync(filePath, JSON.stringify(config, null, 2) + `
123
- `);
124
- } catch {}
125
- }
1
+ import{existsSync as m,mkdirSync as k,readFileSync as E,writeFileSync as G,statSync as g,unlinkSync as I}from"fs";import{join as o}from"path";function S(){return o(process.env.XDG_DATA_HOME||o(process.env.HOME||"~",".local","share"),"opencode","add-dir")}function x(){return o(S(),"directories.json")}function d(){return o(S(),"session-dirs.json")}function C(n){return n.startsWith("~/")?(process.env.HOME||"~")+n.slice(1):n}function y(n){try{return JSON.parse(E(n,"utf-8"))}catch{return[]}}function N(){let n=new Map;for(let t of y(x()))n.set(t,{path:t,persist:!0});for(let t of y(d()))if(!n.has(t))n.set(t,{path:t,persist:!1});return n}var c,h=0,D=0,b=500;function p(){let n=Date.now();if(c&&n-D<b)return c;D=n;let t=0;try{t+=g(x()).mtimeMs}catch{}try{t+=g(d()).mtimeMs}catch{}if(c&&t===h)return c;return h=t,c=N(),c}function F(n,t){return t===n||t.startsWith(n+"/")}function u(n,t){for(let e of n.values())if(F(e.path,t))return!0;return!1}var l="opencode-add-dir",f=o(process.env.XDG_CONFIG_HOME||o(process.env.HOME||"~",".config"),"opencode");function H(n){let t="",e=!1,i=!1;for(let r=0;r<n.length;r++){let s=n[r];if(i){t+=s,i=!1;continue}if(s==="\\"&&e){t+=s,i=!0;continue}if(s==='"'){e=!e,t+=s;continue}if(e){t+=s;continue}if(s==="/"&&n[r+1]==="/"){while(r<n.length&&n[r]!==`
2
+ `)r++;continue}if(s==="/"&&n[r+1]==="*"){r+=2;while(r<n.length&&!(n[r]==="*"&&n[r+1]==="/"))r++;r++;continue}t+=s}return t}function R(){for(let n of["tui.jsonc","tui.json"]){let t=o(f,n);if(m(t))return t}return o(f,"tui.json")}function A(){try{I(d())}catch{}try{if(!m(f))k(f,{recursive:!0});let n=R(),t={};if(m(n))t=JSON.parse(H(E(n,"utf-8")));let e=t.plugin??[];if(e.some((r)=>{let s=Array.isArray(r)?r[0]:r;return s===l||typeof s==="string"&&s.startsWith(l+"@")}))return;t.plugin=[...e,l],G(n,JSON.stringify(t,null,2)+`
3
+ `)}catch{}}import{resolve as J}from"path";var K=new Set(["read","write","edit","apply_patch","multiedit","glob","grep","list","bash"]),T=new Set;function v(n){return n+"/*"}async function w(n,t){if(T.has(t))return;T.add(t),await n.session.prompt({path:{id:t},body:{noReply:!0,tools:{external_directory:!0},parts:[]}}).catch(()=>{})}function M(n,t,e){if(!n.size||!K.has(t))return!1;let i=L(t,e);return!!i&&u(n,J(C(i)))}async function P(n,t,e){if(t.permission!=="external_directory")return;let{filepath:i="",parentDir:r=""}=t.metadata,s=t.patterns??[];if(!(u(e,i)||u(e,r)||s.some((j)=>u(e,j.replace(/\/?\*$/,""))))||!t.id||!t.sessionID)return;await n.postSessionIdPermissionsPermissionId({path:{id:t.sessionID,permissionID:t.id},body:{response:"always"}}).catch(()=>{})}function L(n,t){if(!t)return"";if(n==="bash")return t.workdir||t.command||"";return t.filePath||t.path||t.pattern||""}import{existsSync as $,readFileSync as X}from"fs";import{join as z}from"path";var W=["AGENTS.md","CLAUDE.md",".agents/AGENTS.md"];function B(){return process.env.OPENCODE_ADDDIR_INJECT_CONTEXT==="1"}function O(n){if(!n.size)return[];let e=[`Additional working directories:
4
+ ${[...n.values()].map((i)=>`- ${i.path}`).join(`
5
+ `)}`];if(!B())return e;for(let i of n.values())for(let r of W){let s=z(i.path,r);if(!$(s))continue;let a=X(s,"utf-8").trim();if(a)e.push(`# Context from ${s}
126
6
 
127
- // src/permissions.ts
128
- import { join as join2, resolve } from "path";
129
- var FILE_TOOLS = new Set(["read", "write", "edit", "apply_patch", "multiedit", "glob", "grep", "list", "bash"]);
130
- var grantedSessions = new Set;
131
- function permissionGlob(dirPath) {
132
- return join2(dirPath, "*");
133
- }
134
- async function grantSession(sdk, sessionID, text) {
135
- if (grantedSessions.has(sessionID))
136
- return;
137
- grantedSessions.add(sessionID);
138
- const body = { noReply: true, tools: { external_directory: true }, parts: [{ type: "text", text }] };
139
- await sdk.session.prompt({ path: { id: sessionID }, body }).catch(() => {});
140
- }
141
- function shouldGrantBeforeTool(dirs, tool, args) {
142
- if (!dirs.size || !FILE_TOOLS.has(tool))
143
- return false;
144
- const p = extractPath(tool, args);
145
- return !!p && matchesDirs(dirs, resolve(expandHome(p)));
146
- }
147
- async function autoApprovePermission(sdk, props, dirs) {
148
- if (props.permission !== "external_directory")
149
- return;
150
- const meta = props.metadata;
151
- const filepath = meta.filepath ?? "";
152
- const parentDir = meta.parentDir ?? "";
153
- const patterns = props.patterns ?? [];
154
- const matches = matchesDirs(dirs, filepath) || matchesDirs(dirs, parentDir) || patterns.some((p) => matchesDirs(dirs, p.replace(/\/?\*$/, "")));
155
- if (!matches || !props.id || !props.sessionID)
156
- return;
157
- await sdk.postSessionIdPermissionsPermissionId({
158
- path: { id: props.sessionID, permissionID: props.id },
159
- body: { response: "always" }
160
- }).catch(() => {});
161
- }
162
- function extractPath(tool, args) {
163
- if (!args)
164
- return "";
165
- if (tool === "bash")
166
- return args.workdir || args.command || "";
167
- return args.filePath || args.path || args.pattern || "";
168
- }
169
-
170
- // src/context.ts
171
- import { readFileSync as readFileSync2 } from "fs";
172
- import { join as join3 } from "path";
173
- var CONTEXT_FILES = ["AGENTS.md", "CLAUDE.md", ".agents/AGENTS.md"];
174
- function collectAgentContext(dirs) {
175
- const sections = [];
176
- for (const entry of dirs.values()) {
177
- for (const name of CONTEXT_FILES) {
178
- const fp = join3(entry.path, name);
179
- try {
180
- const content = readFileSync2(fp, "utf-8").trim();
181
- if (content)
182
- sections.push(`# Context from ${fp}
183
-
184
- ${content}`);
185
- } catch {}
186
- }
187
- }
188
- return sections;
189
- }
190
-
191
- // src/plugin.ts
192
- ensureTuiConfig();
193
- var AddDirPlugin = async ({ client }) => {
194
- const sdk = client;
195
- return {
196
- config: async (cfg) => {
197
- const dirs = freshDirs();
198
- if (!dirs.size)
199
- return;
200
- const perm = cfg.permission ??= {};
201
- const extDir = perm.external_directory ??= {};
202
- for (const entry of dirs.values())
203
- extDir[permissionGlob(entry.path)] = "allow";
204
- },
205
- "tool.execute.before": async (input, output) => {
206
- const dirs = freshDirs();
207
- if (shouldGrantBeforeTool(dirs, input.tool, output.args))
208
- await grantSession(sdk, input.sessionID, "Directory access granted by add-dir plugin.");
209
- },
210
- event: async ({ event }) => {
211
- const e = event;
212
- if (e.type === "permission.asked" && e.properties) {
213
- const dirs = freshDirs();
214
- await autoApprovePermission(sdk, e.properties, dirs);
215
- }
216
- },
217
- "experimental.chat.system.transform": async (_input, output) => {
218
- const dirs = freshDirs();
219
- output.system.push(...collectAgentContext(dirs));
220
- }
221
- };
222
- };
223
-
224
- // src/index.ts
225
- var plugin = {
226
- id: "opencode-add-dir",
227
- server: AddDirPlugin
228
- };
229
- var src_default = plugin;
230
- export {
231
- src_default as default
232
- };
7
+ ${a}`)}return e}A();var _=async({client:n})=>{let t=n;return{config:async(e)=>{let i=p();if(!i.size)return;let r=e.permission??={},s=r.external_directory??={};for(let a of i.values())s[v(a.path)]="allow"},"tool.execute.before":async(e,i)=>{let r=p();if(M(r,e.tool,i.args))await w(t,e.sessionID)},event:async({event:e})=>{let i=e;if(i.type==="permission.asked"&&i.properties){let r=p();await P(t,i.properties,r)}},"experimental.chat.system.transform":async(e,i)=>{let r=p();i.system.push(...O(r))}}};var U={id:"opencode-add-dir",server:_},ut=U;export{ut as default};
@@ -1,7 +1,8 @@
1
1
  import type { DirEntry } from "./state.js";
2
2
  import type { SDK, PermissionEvent, ToolArgs } from "./types.js";
3
+ export declare function resetGrantedSessions(): void;
3
4
  export declare function permissionGlob(dirPath: string): string;
4
- export declare function grantSession(sdk: SDK, sessionID: string, text: string): Promise<void>;
5
+ export declare function grantSession(sdk: SDK, sessionID: string): Promise<void>;
5
6
  export declare function shouldGrantBeforeTool(dirs: Map<string, DirEntry>, tool: string, args: ToolArgs): boolean;
6
7
  export declare function autoApprovePermission(sdk: SDK, props: PermissionEvent, dirs: Map<string, DirEntry>): Promise<void>;
7
8
  export declare function extractPath(tool: string, args: ToolArgs): string;
package/dist/tui.tsx CHANGED
@@ -5,16 +5,21 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from "fs
5
5
  import { join, resolve } from "path"
6
6
 
7
7
  const ID = "opencode-add-dir"
8
- const DIRS_FILE = () => join(process.env["XDG_DATA_HOME"] || join(process.env["HOME"] || "~", ".local", "share"), "opencode", "add-dir", "directories.json")
8
+ const STATE_DIR = join(process.env["XDG_DATA_HOME"] || join(process.env["HOME"] || "~", ".local", "share"), "opencode", "add-dir")
9
+ const PERSISTED_FILE = join(STATE_DIR, "directories.json")
10
+ const SESSION_FILE = join(STATE_DIR, "session-dirs.json")
9
11
 
10
- function readDirs(): string[] {
11
- try { return JSON.parse(readFileSync(DIRS_FILE(), "utf-8")) } catch { return [] }
12
+ function readJsonArray(file: string): string[] {
13
+ try { return JSON.parse(readFileSync(file, "utf-8")) } catch { return [] }
12
14
  }
13
15
 
14
- function writeDirs(dirs: string[]) {
15
- const dir = join(DIRS_FILE(), "..")
16
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
17
- writeFileSync(DIRS_FILE(), JSON.stringify(dirs, null, 2))
16
+ function writeJsonArray(file: string, items: string[]) {
17
+ if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true })
18
+ writeFileSync(file, JSON.stringify(items, null, 2))
19
+ }
20
+
21
+ function allDirs(): string[] {
22
+ return [...new Set([...readJsonArray(PERSISTED_FILE), ...readJsonArray(SESSION_FILE)])]
18
23
  }
19
24
 
20
25
  function resolvePath(input: string) {
@@ -23,20 +28,33 @@ function resolvePath(input: string) {
23
28
  }
24
29
 
25
30
  function validate(input: string): string | undefined {
26
- if (!input.trim()) return "No directory path provided."
31
+ if (!input.trim()) return "Path is required."
27
32
  const abs = resolvePath(input)
28
- try { if (!statSync(abs).isDirectory()) return `${abs} is not a directory.` }
29
- catch { return `Path ${abs} was not found.` }
30
- if (readDirs().includes(abs)) return `${abs} is already added.`
33
+ try { if (!statSync(abs).isDirectory()) return `Not a directory: ${abs}` }
34
+ catch { return `Not found: ${abs}` }
35
+ if (allDirs().includes(abs)) return `Already added: ${abs}`
36
+ }
37
+
38
+ function addDir(abs: string, persist: boolean) {
39
+ const file = persist ? PERSISTED_FILE : SESSION_FILE
40
+ const dirs = readJsonArray(file)
41
+ if (!dirs.includes(abs)) writeJsonArray(file, [...dirs, abs])
42
+ }
43
+
44
+ function removeDir(path: string) {
45
+ for (const file of [PERSISTED_FILE, SESSION_FILE]) {
46
+ const dirs = readJsonArray(file)
47
+ if (dirs.includes(path)) writeJsonArray(file, dirs.filter((d) => d !== path))
48
+ }
31
49
  }
32
50
 
33
- function sessionID(api: TuiPluginApi): string | undefined {
51
+ function getSessionID(api: TuiPluginApi): string | undefined {
34
52
  const r = api.route.current
35
53
  return r.name === "session" && r.params ? r.params.sessionID as string : undefined
36
54
  }
37
55
 
38
- async function withSession(api: TuiPluginApi): Promise<string | undefined> {
39
- const id = sessionID(api)
56
+ async function ensureSession(api: TuiPluginApi): Promise<string | undefined> {
57
+ const id = getSessionID(api)
40
58
  if (id) return id
41
59
  const res = await api.client.session.create({})
42
60
  if (res.error) return
@@ -44,20 +62,6 @@ async function withSession(api: TuiPluginApi): Promise<string | undefined> {
44
62
  return res.data.id
45
63
  }
46
64
 
47
- type PromptAsyncFn = (params: {
48
- sessionID: string
49
- parts: { type: "text"; text: string }[]
50
- noReply: boolean
51
- tools: Record<string, boolean>
52
- }) => Promise<unknown>
53
-
54
- async function grant(api: TuiPluginApi, sid: string, msg: string) {
55
- const promptAsync = (api.client.session as unknown as { promptAsync: PromptAsyncFn }).promptAsync
56
- await promptAsync({
57
- sessionID: sid, parts: [{ type: "text", text: msg }], noReply: true, tools: { external_directory: true },
58
- }).catch(() => {})
59
- }
60
-
61
65
  function AddDirDialog(props: { api: TuiPluginApi }) {
62
66
  const [busy, setBusy] = createSignal(false)
63
67
  const [remember, setRemember] = createSignal(false)
@@ -65,7 +69,8 @@ function AddDirDialog(props: { api: TuiPluginApi }) {
65
69
 
66
70
  useKeyboard((e) => {
67
71
  if (e.name !== "tab" || busy()) return
68
- e.preventDefault(); e.stopPropagation()
72
+ e.preventDefault()
73
+ e.stopPropagation()
69
74
  setRemember((v) => !v)
70
75
  })
71
76
 
@@ -74,44 +79,56 @@ function AddDirDialog(props: { api: TuiPluginApi }) {
74
79
  title="Add directory"
75
80
  placeholder="/path/to/directory"
76
81
  busy={busy()}
77
- busyText="Adding directory..."
82
+ busyText="Adding..."
78
83
  description={() => (
79
84
  <box gap={1}>
80
85
  <box gap={0}>
81
- <text fg={api.theme.current.textMuted}>To get the full path of a project:</text>
82
- <text fg={api.theme.current.textMuted}> 1. Move to the project in your terminal</text>
83
- <text fg={api.theme.current.textMuted}> 2. Run "pwd" and copy the output</text>
86
+ <text fg={api.theme.current.textMuted}>How to get the full path:</text>
87
+ <text fg={api.theme.current.textMuted}> 1. cd to the project in your terminal</text>
88
+ <text fg={api.theme.current.textMuted}> 2. Run "pwd", copy the output</text>
84
89
  <text fg={api.theme.current.textMuted}> 3. Paste below</text>
85
90
  </box>
86
91
  <box flexDirection="row" gap={1}>
87
92
  <text fg={remember() ? api.theme.current.text : api.theme.current.textMuted}>
88
93
  {remember() ? "[x]" : "[ ]"} Remember across sessions
89
94
  </text>
90
- <text fg={api.theme.current.textMuted}>(tab toggle)</text>
95
+ <text fg={api.theme.current.textMuted}>(tab)</text>
91
96
  </box>
92
97
  </box>
93
98
  )}
94
99
  onConfirm={async (value) => {
100
+ if (busy()) return
95
101
  const err = validate(value)
96
102
  if (err) return api.ui.toast({ variant: "error", message: err })
97
103
 
104
+ const abs = resolvePath(value)
105
+ const persist = remember()
106
+
98
107
  setBusy(true)
99
- try {
100
- const sid = await withSession(api)
101
- if (!sid) return api.ui.toast({ variant: "error", message: "Failed to create session" })
102
- const abs = resolvePath(value)
103
- if (remember()) { const d = readDirs(); if (!d.includes(abs)) writeDirs([...d, abs]) }
104
- await grant(api, sid, `Added ${abs} as a working directory (${remember() ? "persistent" : "session"}).`)
105
- api.ui.dialog.clear()
106
- } finally { setBusy(false) }
108
+ const sid = await ensureSession(api)
109
+ if (!sid) {
110
+ setBusy(false)
111
+ return api.ui.toast({ variant: "error", message: "Failed to create session" })
112
+ }
113
+
114
+ addDir(abs, persist)
115
+ api.ui.dialog.clear()
116
+
117
+ const label = persist ? "persistent" : "session"
118
+ api.client.session.prompt({
119
+ sessionID: sid,
120
+ parts: [{ type: "text", text: `Added ${abs} as a working directory (${label}).`, ignored: true }],
121
+ noReply: true,
122
+ tools: { external_directory: true },
123
+ }).catch(() => {})
107
124
  }}
108
125
  onCancel={() => api.ui.dialog.clear()}
109
126
  />
110
127
  )
111
128
  }
112
129
 
113
- function listDirs(api: TuiPluginApi) {
114
- const dirs = readDirs()
130
+ function showListDirs(api: TuiPluginApi) {
131
+ const dirs = allDirs()
115
132
  if (!dirs.length) return api.ui.toast({ variant: "info", message: "No directories added." })
116
133
  api.ui.dialog.replace(() => (
117
134
  <api.ui.DialogAlert
@@ -122,8 +139,8 @@ function listDirs(api: TuiPluginApi) {
122
139
  ))
123
140
  }
124
141
 
125
- function removeDir(api: TuiPluginApi) {
126
- const dirs = readDirs()
142
+ function showRemoveDir(api: TuiPluginApi) {
143
+ const dirs = allDirs()
127
144
  if (!dirs.length) return api.ui.toast({ variant: "info", message: "No directories to remove." })
128
145
  api.ui.dialog.replace(() => (
129
146
  <api.ui.DialogSelect
@@ -135,11 +152,11 @@ function removeDir(api: TuiPluginApi) {
135
152
  title="Remove directory"
136
153
  message={`Remove ${opt.value}?`}
137
154
  onConfirm={() => {
138
- writeDirs(readDirs().filter((d) => d !== opt.value))
139
- api.ui.toast({ variant: "success", message: `Removed ${opt.value}` })
155
+ removeDir(opt.value as string)
140
156
  api.ui.dialog.clear()
157
+ api.ui.toast({ variant: "success", message: `Removed ${opt.value}` })
141
158
  }}
142
- onCancel={() => removeDir(api)}
159
+ onCancel={() => showRemoveDir(api)}
143
160
  />
144
161
  ))
145
162
  }}
@@ -150,8 +167,8 @@ function removeDir(api: TuiPluginApi) {
150
167
  const tui: TuiPlugin = async (api) => {
151
168
  api.command.register(() => [
152
169
  { title: "Add directory", value: "add-dir", description: "Add a working directory", category: "Directories", slash: { name: "add-dir" }, onSelect: () => api.ui.dialog.replace(() => <AddDirDialog api={api} />) },
153
- { title: "List directories", value: "list-dir", description: "Show working directories", category: "Directories", slash: { name: "list-dir" }, onSelect: () => listDirs(api) },
154
- { title: "Remove directory", value: "remove-dir", description: "Remove a working directory", category: "Directories", slash: { name: "remove-dir" }, onSelect: () => removeDir(api) },
170
+ { title: "List directories", value: "list-dir", description: "Show working directories", category: "Directories", slash: { name: "list-dir" }, onSelect: () => showListDirs(api) },
171
+ { title: "Remove directory", value: "remove-dir", description: "Remove a working directory", category: "Directories", slash: { name: "remove-dir" }, onSelect: () => showRemoveDir(api) },
155
172
  ])
156
173
  }
157
174
 
package/dist/types.d.ts CHANGED
@@ -1,13 +1,5 @@
1
1
  import type { PluginInput } from "@opencode-ai/plugin";
2
2
  export type SDK = PluginInput["client"];
3
- export interface PromptBody {
4
- noReply: true;
5
- tools?: Record<string, boolean>;
6
- parts: Array<{
7
- type: "text";
8
- text: string;
9
- }>;
10
- }
11
3
  export interface PermissionEvent {
12
4
  id: string;
13
5
  sessionID: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-add-dir",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Add working directories to your OpenCode session with auto-approved permissions",
5
5
  "author": "Cristian Fonseca <cfonsecacomas@gmail.com>",
6
6
  "type": "module",
@@ -27,17 +27,15 @@
27
27
  },
28
28
  "files": [
29
29
  "dist/",
30
- "bin/ensure-tui.mjs",
31
30
  "bin/setup.mjs",
32
31
  "README.md",
33
32
  "LICENSE"
34
33
  ],
35
34
  "scripts": {
36
- "build": "bun run build:server && bun run build:tui && bun x tsc --emitDeclarationOnly",
37
- "build:server": "bun build ./src/index.ts --outdir ./dist --target node --format esm --external @opencode-ai/plugin",
35
+ "build": "rm -rf dist && bun run build:server && bun run build:tui && bun x tsc --emitDeclarationOnly",
36
+ "build:server": "bun build ./src/index.ts --outdir ./dist --target node --format esm --external @opencode-ai/plugin --minify",
38
37
  "build:tui": "cp ./src/tui-plugin.tsx ./dist/tui.tsx",
39
- "postinstall": "node ./bin/ensure-tui.mjs",
40
- "deploy": "bun run build:server && bun run build:tui",
38
+ "deploy": "rm -rf dist && bun run build:server && bun run build:tui",
41
39
  "test": "bun test",
42
40
  "typecheck": "bun x tsc --noEmit",
43
41
  "prepublishOnly": "bun run typecheck && bun test && bun run build"
@@ -1,39 +0,0 @@
1
- #!/usr/bin/env node
2
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"
3
- import { join } from "path"
4
- import { homedir } from "os"
5
-
6
- const PKG = "opencode-add-dir"
7
-
8
- try {
9
- const dir = join(
10
- process.env.XDG_CONFIG_HOME || join(homedir(), ".config"),
11
- "opencode",
12
- )
13
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
14
-
15
- let filePath = join(dir, "tui.json")
16
- for (const name of ["tui.jsonc", "tui.json"]) {
17
- const p = join(dir, name)
18
- if (existsSync(p)) { filePath = p; break }
19
- }
20
-
21
- let config = {}
22
- if (existsSync(filePath)) {
23
- const raw = readFileSync(filePath, "utf-8")
24
- config = JSON.parse(raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""))
25
- }
26
-
27
- const plugins = config.plugin || []
28
- const has = plugins.some((p) => {
29
- const name = Array.isArray(p) ? p[0] : p
30
- return name === PKG || (typeof name === "string" && name.startsWith(PKG + "@"))
31
- })
32
-
33
- if (!has) {
34
- config.plugin = [...plugins, PKG]
35
- writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n")
36
- }
37
- } catch {
38
- // Non-critical — TUI dialog available after manual setup or restart
39
- }