@2en/clawly-plugins 1.10.0 → 1.12.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/gateway/memory.ts +99 -77
- package/package.json +1 -1
package/gateway/memory.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Memory
|
|
2
|
+
* Memory & workspace browser — expose workspace .md files as Gateway RPC methods.
|
|
3
3
|
*
|
|
4
4
|
* Gateway methods:
|
|
5
|
-
* - memory-browser.list — list
|
|
6
|
-
* - memory-browser.get —
|
|
5
|
+
* - memory-browser.list — list .md files in memory directory (may be shadowed by built-in)
|
|
6
|
+
* - memory-browser.get — read a .md file from memory directory (may be shadowed by built-in)
|
|
7
|
+
* - clawly.workspace.list — list root-level .md files in workspace directory
|
|
8
|
+
* - clawly.workspace.get — read a .md file from workspace root
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import fs from 'node:fs/promises'
|
|
@@ -108,29 +110,84 @@ async function listRootMdFiles(dir: string): Promise<string[]> {
|
|
|
108
110
|
.sort()
|
|
109
111
|
}
|
|
110
112
|
|
|
113
|
+
/** Validate path and read a .md file, responding via the RPC respond callback. */
|
|
114
|
+
async function readMdFile(
|
|
115
|
+
dir: string,
|
|
116
|
+
relativePath: string | undefined,
|
|
117
|
+
api: PluginApi,
|
|
118
|
+
respond: (ok: boolean, payload?: unknown, error?: {code: string; message: string}) => void,
|
|
119
|
+
) {
|
|
120
|
+
if (!relativePath) {
|
|
121
|
+
respond(false, undefined, {
|
|
122
|
+
code: 'invalid_params',
|
|
123
|
+
message: 'path (relative path to .md file) is required',
|
|
124
|
+
})
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
if (!isSafeRelativePath(relativePath)) {
|
|
128
|
+
respond(false, undefined, {
|
|
129
|
+
code: 'invalid_params',
|
|
130
|
+
message: 'path must be a safe relative path (no directory traversal)',
|
|
131
|
+
})
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
if (!relativePath.toLowerCase().endsWith('.md')) {
|
|
135
|
+
respond(false, undefined, {
|
|
136
|
+
code: 'invalid_params',
|
|
137
|
+
message: 'path must point to a .md file',
|
|
138
|
+
})
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const fullPath = path.join(dir, path.normalize(relativePath))
|
|
143
|
+
const realBase = await fs.realpath(dir).catch(() => dir)
|
|
144
|
+
const realResolved = await fs.realpath(fullPath).catch(() => null)
|
|
145
|
+
|
|
146
|
+
if (!realResolved) {
|
|
147
|
+
respond(false, undefined, {code: 'not_found', message: 'file not found'})
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
const rel = path.relative(realBase, realResolved)
|
|
151
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
152
|
+
respond(false, undefined, {
|
|
153
|
+
code: 'not_found',
|
|
154
|
+
message: 'file not found or outside allowed directory',
|
|
155
|
+
})
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const content = await fs.readFile(fullPath, 'utf-8')
|
|
161
|
+
respond(true, {path: relativePath, content})
|
|
162
|
+
} catch (err) {
|
|
163
|
+
const code = (err as NodeJS.ErrnoException).code
|
|
164
|
+
if (code === 'ENOENT') {
|
|
165
|
+
respond(false, undefined, {code: 'not_found', message: 'file not found'})
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
api.logger.error(`readMdFile failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
169
|
+
respond(false, undefined, {
|
|
170
|
+
code: 'error',
|
|
171
|
+
message: err instanceof Error ? err.message : String(err),
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
111
176
|
export function registerMemoryBrowser(api: PluginApi) {
|
|
112
177
|
const memoryDir = resolveMemoryDir(api)
|
|
113
|
-
|
|
178
|
+
const wsRoot = resolveWorkspaceRoot(api)
|
|
179
|
+
api.logger.info(`memory-browser: memory=${memoryDir} workspace=${wsRoot}`)
|
|
114
180
|
|
|
115
181
|
// -----------------------------
|
|
116
|
-
// memory-browser.list
|
|
182
|
+
// memory-browser.list (may be shadowed by built-in)
|
|
117
183
|
// -----------------------------
|
|
118
184
|
api.registerGatewayMethod('memory-browser.list', async ({params, respond}) => {
|
|
119
185
|
try {
|
|
120
186
|
const profile = readString(params, 'profile')
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const wsRoot = resolveWorkspaceRoot(api, profile)
|
|
126
|
-
files = await listRootMdFiles(wsRoot)
|
|
127
|
-
api.logger.info(`memory-browser.list (root): ${files.length} files found in ${wsRoot}`)
|
|
128
|
-
} else {
|
|
129
|
-
const dir = profile ? resolveMemoryDir(api, profile) : memoryDir
|
|
130
|
-
files = await collectMdFiles(dir, dir)
|
|
131
|
-
files.sort()
|
|
132
|
-
api.logger.info(`memory-browser.list: ${files.length} files found`)
|
|
133
|
-
}
|
|
187
|
+
const dir = profile ? resolveMemoryDir(api, profile) : memoryDir
|
|
188
|
+
const files = await collectMdFiles(dir, dir)
|
|
189
|
+
files.sort()
|
|
190
|
+
api.logger.info(`memory-browser.list: ${files.length} files found`)
|
|
134
191
|
respond(true, {files})
|
|
135
192
|
} catch (err) {
|
|
136
193
|
api.logger.error(
|
|
@@ -144,71 +201,27 @@ export function registerMemoryBrowser(api: PluginApi) {
|
|
|
144
201
|
})
|
|
145
202
|
|
|
146
203
|
// -----------------------------
|
|
147
|
-
// memory-browser.get
|
|
204
|
+
// memory-browser.get (may be shadowed by built-in)
|
|
148
205
|
// -----------------------------
|
|
149
206
|
api.registerGatewayMethod('memory-browser.get', async ({params, respond}) => {
|
|
150
|
-
const relativePath = readString(params, 'path')
|
|
151
207
|
const profile = readString(params, 'profile')
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
? resolveWorkspaceRoot(api, profile)
|
|
156
|
-
: profile
|
|
157
|
-
? resolveMemoryDir(api, profile)
|
|
158
|
-
: memoryDir
|
|
159
|
-
if (!relativePath) {
|
|
160
|
-
respond(false, undefined, {
|
|
161
|
-
code: 'invalid_params',
|
|
162
|
-
message: 'path (relative path to .md file) is required',
|
|
163
|
-
})
|
|
164
|
-
return
|
|
165
|
-
}
|
|
166
|
-
if (!isSafeRelativePath(relativePath)) {
|
|
167
|
-
respond(false, undefined, {
|
|
168
|
-
code: 'invalid_params',
|
|
169
|
-
message: 'path must be a safe relative path (no directory traversal)',
|
|
170
|
-
})
|
|
171
|
-
return
|
|
172
|
-
}
|
|
173
|
-
if (!relativePath.toLowerCase().endsWith('.md')) {
|
|
174
|
-
respond(false, undefined, {
|
|
175
|
-
code: 'invalid_params',
|
|
176
|
-
message: 'path must point to a .md file',
|
|
177
|
-
})
|
|
178
|
-
return
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const fullPath = path.join(dir, path.normalize(relativePath))
|
|
182
|
-
const realMemory = await fs.realpath(dir).catch(() => dir)
|
|
183
|
-
const realResolved = await fs.realpath(fullPath).catch(() => null)
|
|
184
|
-
|
|
185
|
-
if (!realResolved) {
|
|
186
|
-
respond(false, undefined, {
|
|
187
|
-
code: 'not_found',
|
|
188
|
-
message: 'file not found',
|
|
189
|
-
})
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
|
-
const rel = path.relative(realMemory, realResolved)
|
|
193
|
-
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
194
|
-
respond(false, undefined, {
|
|
195
|
-
code: 'not_found',
|
|
196
|
-
message: 'file not found or outside memory directory',
|
|
197
|
-
})
|
|
198
|
-
return
|
|
199
|
-
}
|
|
208
|
+
const dir = profile ? resolveMemoryDir(api, profile) : memoryDir
|
|
209
|
+
await readMdFile(dir, readString(params, 'path'), api, respond)
|
|
210
|
+
})
|
|
200
211
|
|
|
212
|
+
// -----------------------------
|
|
213
|
+
// clawly.workspace.list — workspace root .md files (not shadowed)
|
|
214
|
+
// -----------------------------
|
|
215
|
+
api.registerGatewayMethod('clawly.workspace.list', async ({params, respond}) => {
|
|
201
216
|
try {
|
|
202
|
-
const
|
|
203
|
-
|
|
217
|
+
const profile = readString(params, 'profile')
|
|
218
|
+
const root = resolveWorkspaceRoot(api, profile)
|
|
219
|
+
const files = await listRootMdFiles(root)
|
|
220
|
+
api.logger.info(`clawly.workspace.list: ${files.length} files in ${root}`)
|
|
221
|
+
respond(true, {files})
|
|
204
222
|
} catch (err) {
|
|
205
|
-
const code = (err as NodeJS.ErrnoException).code
|
|
206
|
-
if (code === 'ENOENT') {
|
|
207
|
-
respond(false, undefined, {code: 'not_found', message: 'file not found'})
|
|
208
|
-
return
|
|
209
|
-
}
|
|
210
223
|
api.logger.error(
|
|
211
|
-
`
|
|
224
|
+
`clawly.workspace.list failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
212
225
|
)
|
|
213
226
|
respond(false, undefined, {
|
|
214
227
|
code: 'error',
|
|
@@ -217,5 +230,14 @@ export function registerMemoryBrowser(api: PluginApi) {
|
|
|
217
230
|
}
|
|
218
231
|
})
|
|
219
232
|
|
|
220
|
-
|
|
233
|
+
// -----------------------------
|
|
234
|
+
// clawly.workspace.get — read a single .md file from workspace root
|
|
235
|
+
// -----------------------------
|
|
236
|
+
api.registerGatewayMethod('clawly.workspace.get', async ({params, respond}) => {
|
|
237
|
+
const profile = readString(params, 'profile')
|
|
238
|
+
const root = resolveWorkspaceRoot(api, profile)
|
|
239
|
+
await readMdFile(root, readString(params, 'path'), api, respond)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
api.logger.info('memory-browser: registered')
|
|
221
243
|
}
|