picocode-core 0.9.119

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.
Files changed (57) hide show
  1. package/README.md +15 -0
  2. package/package.json +33 -0
  3. package/src/agent-transcript.js +80 -0
  4. package/src/agent.js +170 -0
  5. package/src/agents.js +213 -0
  6. package/src/attachments.js +141 -0
  7. package/src/boot.js +28 -0
  8. package/src/catalog-snapshot.json +1 -0
  9. package/src/catalog.js +86 -0
  10. package/src/codex-models.js +56 -0
  11. package/src/commands.js +61 -0
  12. package/src/compaction.js +82 -0
  13. package/src/completion.js +21 -0
  14. package/src/config.js +32 -0
  15. package/src/context.js +82 -0
  16. package/src/controller.js +1263 -0
  17. package/src/conversation-search.js +101 -0
  18. package/src/deliberation-history.js +65 -0
  19. package/src/deliberation.js +61 -0
  20. package/src/derive.js +307 -0
  21. package/src/events.js +54 -0
  22. package/src/export.js +16 -0
  23. package/src/files.js +39 -0
  24. package/src/format.js +6 -0
  25. package/src/fuzzy.js +41 -0
  26. package/src/git.js +156 -0
  27. package/src/history.js +51 -0
  28. package/src/init.js +25 -0
  29. package/src/keys.js +26 -0
  30. package/src/mcp.js +280 -0
  31. package/src/memory.js +120 -0
  32. package/src/models.js +14 -0
  33. package/src/openai-auth.js +204 -0
  34. package/src/paths.js +67 -0
  35. package/src/reversible-edit.js +79 -0
  36. package/src/rewind.js +84 -0
  37. package/src/session-index.js +264 -0
  38. package/src/session-lock.js +27 -0
  39. package/src/session.js +160 -0
  40. package/src/shells.js +166 -0
  41. package/src/skills.js +164 -0
  42. package/src/steer.js +129 -0
  43. package/src/system-prompt.js +49 -0
  44. package/src/terminal-theme.js +49 -0
  45. package/src/tools/bash.js +184 -0
  46. package/src/tools/diff.js +18 -0
  47. package/src/tools/edit.js +95 -0
  48. package/src/tools/glob.js +43 -0
  49. package/src/tools/grep.js +59 -0
  50. package/src/tools/index.js +296 -0
  51. package/src/tools/read.js +49 -0
  52. package/src/tools/recorder.js +74 -0
  53. package/src/tools/web.js +84 -0
  54. package/src/tools/write.js +48 -0
  55. package/src/update.js +82 -0
  56. package/src/user-tools.js +59 -0
  57. package/src/wakeups.js +40 -0
package/src/fuzzy.js ADDED
@@ -0,0 +1,41 @@
1
+ export function fuzzyScore(query, path) {
2
+ const q = query.toLowerCase()
3
+ const s = path.toLowerCase()
4
+ if (!q) return 0
5
+
6
+ const substringIndex = s.indexOf(q)
7
+ if (substringIndex !== -1) return 10_000 - substringIndex - s.length / 10_000
8
+
9
+ let best = -1
10
+ for (let start = s.indexOf(q[0]); start !== -1; start = s.indexOf(q[0], start + 1)) {
11
+ let score = 0
12
+ let prev = -1
13
+ let matched = true
14
+ for (const ch of q) {
15
+ const i = s.indexOf(ch, prev === -1 ? start : prev + 1)
16
+ if (i === -1) {
17
+ matched = false
18
+ break
19
+ }
20
+ if (prev !== -1 && i === prev + 1) score += 3
21
+ if (i === 0 || '/.-_'.includes(s[i - 1])) score += 2
22
+ score += 1
23
+ prev = i
24
+ }
25
+ if (matched && score > best) best = score
26
+ }
27
+ if (best < 0) return -1
28
+ const base = s.slice(s.lastIndexOf('/') + 1)
29
+ if (base.startsWith(q)) best += 8
30
+ else if (base.includes(q)) best += 5
31
+ return best - s.length / 100
32
+ }
33
+
34
+ export function rankFuzzy(items, query, score) {
35
+ if (!query) return items
36
+ return items
37
+ .map((item, index) => ({ item, index, score: score(query, item) }))
38
+ .filter((entry) => entry.score >= 0)
39
+ .sort((a, b) => b.score - a.score || a.index - b.index)
40
+ .map((entry) => entry.item)
41
+ }
package/src/git.js ADDED
@@ -0,0 +1,156 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { readFileSync, statSync, watch } from 'node:fs'
3
+ import { dirname, isAbsolute, join, resolve } from 'node:path'
4
+
5
+ const DEBOUNCE_MS = 500
6
+ const POLL_MS = 15000
7
+ const GIT_TIMEOUT_MS = 5000
8
+
9
+ function findGitDir(startDir) {
10
+ let dir = startDir
11
+ while (true) {
12
+ const candidate = join(dir, '.git')
13
+ try {
14
+ const info = statSync(candidate)
15
+ if (info.isDirectory()) return candidate
16
+ const text = readFileSync(candidate, 'utf-8')
17
+ const match = text.match(/^gitdir:\s*(.+?)\s*$/m)
18
+ if (match) return isAbsolute(match[1]) ? match[1] : resolve(dir, match[1])
19
+ } catch {}
20
+ const parent = dirname(dir)
21
+ if (parent === dir) return null
22
+ dir = parent
23
+ }
24
+ }
25
+
26
+ function readBranch(gitDir) {
27
+ try {
28
+ const head = readFileSync(join(gitDir, 'HEAD'), 'utf-8').trim()
29
+ const ref = head.match(/^ref: refs\/heads\/(.+)$/)
30
+ return ref ? ref[1] : head.slice(0, 7)
31
+ } catch {
32
+ return null
33
+ }
34
+ }
35
+
36
+ function parseShortstat(output) {
37
+ const added = output.match(/(\d+) insertion/)
38
+ const removed = output.match(/(\d+) deletion/)
39
+ return {
40
+ added: added ? Number(added[1]) : 0,
41
+ removed: removed ? Number(removed[1]) : 0,
42
+ }
43
+ }
44
+
45
+ export function createGitService({ onChange = () => {} } = {}) {
46
+ let enabled = false
47
+ let root = null
48
+ let gitDir = null
49
+ let epoch = 0
50
+ let watcher = null
51
+ let poll = null
52
+ let debounce = null
53
+ let child = null
54
+ let rerun = false
55
+ let current = null
56
+
57
+ function teardown() {
58
+ epoch += 1
59
+ watcher?.close()
60
+ watcher = null
61
+ if (poll) clearInterval(poll)
62
+ poll = null
63
+ if (debounce) clearTimeout(debounce)
64
+ debounce = null
65
+ child?.kill('SIGKILL')
66
+ child = null
67
+ rerun = false
68
+ if (current) {
69
+ current = null
70
+ onChange()
71
+ }
72
+ }
73
+
74
+ function setup() {
75
+ teardown()
76
+ if (!enabled || !root) return
77
+ gitDir = findGitDir(root)
78
+ if (!gitDir) return
79
+ const started = epoch
80
+ try {
81
+ watcher = watch(gitDir, () => {
82
+ if (epoch === started) refresh()
83
+ })
84
+ watcher.on('error', () => {})
85
+ } catch {
86
+ watcher = null
87
+ }
88
+ poll = setInterval(run, POLL_MS)
89
+ poll.unref?.()
90
+ run()
91
+ }
92
+
93
+ function refresh() {
94
+ if (!enabled || !gitDir || debounce) return
95
+ debounce = setTimeout(() => {
96
+ debounce = null
97
+ run()
98
+ }, DEBOUNCE_MS)
99
+ debounce.unref?.()
100
+ }
101
+
102
+ function run() {
103
+ if (!enabled || !gitDir || !root) return
104
+ if (child) {
105
+ rerun = true
106
+ return
107
+ }
108
+ const started = epoch
109
+ const branch = readBranch(gitDir)
110
+ const proc = spawn('git', ['--no-optional-locks', 'diff', 'HEAD', '--shortstat'], {
111
+ cwd: root,
112
+ stdio: ['ignore', 'pipe', 'ignore'],
113
+ })
114
+ child = proc
115
+ let output = ''
116
+ proc.stdout.on('data', (chunk) => {
117
+ output += chunk
118
+ })
119
+ const timeout = setTimeout(() => proc.kill('SIGKILL'), GIT_TIMEOUT_MS)
120
+ timeout.unref?.()
121
+ let finished = false
122
+ const done = (code) => {
123
+ if (finished) return
124
+ finished = true
125
+ clearTimeout(timeout)
126
+ if (child === proc) child = null
127
+ if (epoch !== started) return
128
+ const stats = code === 0 ? parseShortstat(output) : { added: 0, removed: 0 }
129
+ const next = branch ? { branch, ...stats } : null
130
+ if (JSON.stringify(next) !== JSON.stringify(current)) {
131
+ current = next
132
+ onChange()
133
+ }
134
+ if (rerun) {
135
+ rerun = false
136
+ refresh()
137
+ }
138
+ }
139
+ proc.on('close', done)
140
+ proc.on('error', () => done(-1))
141
+ }
142
+
143
+ return {
144
+ status: () => current,
145
+ refresh,
146
+ retarget(nextRoot) {
147
+ root = nextRoot
148
+ setup()
149
+ },
150
+ setEnabled(value) {
151
+ enabled = value === true
152
+ setup()
153
+ },
154
+ dispose: teardown,
155
+ }
156
+ }
package/src/history.js ADDED
@@ -0,0 +1,51 @@
1
+ import { appendFile, readFile, readdir } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import { picoHome, projectDir, projectHistoryFile, ensureDir } from './paths.js'
4
+
5
+ const MAX_PROMPTS_PER_SCOPE = 1000
6
+
7
+ export async function appendPrompt(root, text) {
8
+ ensureDir(projectDir(root))
9
+ await appendFile(projectHistoryFile(root), JSON.stringify({ text, at: Date.now() }) + '\n')
10
+ }
11
+
12
+ function parsePromptLines(text) {
13
+ return text
14
+ .split('\n')
15
+ .map((line) => {
16
+ try {
17
+ return line.trim() ? JSON.parse(line) : null
18
+ } catch {
19
+ return null
20
+ }
21
+ })
22
+ .filter(Boolean)
23
+ }
24
+
25
+ async function readPrompts(file, scope) {
26
+ try {
27
+ const entries = parsePromptLines(await readFile(file, 'utf-8'))
28
+ return entries
29
+ .filter((e) => typeof e.text === 'string' && e.text.trim())
30
+ .slice(-MAX_PROMPTS_PER_SCOPE)
31
+ .map((e) => ({ text: e.text, at: e.at || 0, scope }))
32
+ } catch {
33
+ return []
34
+ }
35
+ }
36
+
37
+ export function loadProjectPrompts(root) {
38
+ return readPrompts(projectHistoryFile(root), 'project')
39
+ }
40
+
41
+ export async function loadGlobalPrompts() {
42
+ const projectsDir = join(picoHome(), 'projects')
43
+ let projects = []
44
+ try {
45
+ projects = await readdir(projectsDir)
46
+ } catch {}
47
+ const nested = await Promise.all(
48
+ projects.map((p) => readPrompts(join(projectsDir, p, 'history.jsonl'), 'everywhere')),
49
+ )
50
+ return nested.flat().sort((a, b) => b.at - a.at).slice(0, MAX_PROMPTS_PER_SCOPE)
51
+ }
package/src/init.js ADDED
@@ -0,0 +1,25 @@
1
+ export const INIT_PROMPT = `Initialize or improve this repository's AGENTS.md guidance for future pico sessions.
2
+
3
+ Do this discovery yourself as the main agent. Do not call agent_plan, agent_start, agent_list, agent_collect, or delegate any part of this task to another agent.
4
+
5
+ Inspect the repository directly and then create or update its AGENTS.md files. Read enough of the actual project to understand its structure and workflows, including the relevant manifests, documentation, configuration, source layout, tests, scripts, and existing instruction files. Use targeted searches and representative files rather than exhaustively reading every file. If an AGENTS.md already exists, preserve useful human-authored guidance and improve it conservatively instead of replacing it blindly. Check CLAUDE.md files for useful repository guidance when relevant, but write canonical guidance to AGENTS.md.
6
+
7
+ Write instructions that help an agent work correctly in this specific repository. Capture only durable, non-obvious information such as:
8
+
9
+ - the project's purpose and high-level architecture
10
+ - important module boundaries and data flow
11
+ - authoritative build, test, lint, typecheck, and development commands
12
+ - repository-specific conventions and constraints
13
+ - where and how to add common kinds of changes
14
+ - validation expectations and operational gotchas
15
+
16
+ Keep the root AGENTS.md concise: aim for 100-200 lines or roughly 1,000-2,000 tokens, and treat about 3,000 tokens as a hard default ceiling. This is guidance, not generated documentation. Prefer terse sections and bullets. Omit generic programming advice, exhaustive directory listings, dependency inventories, and facts that are obvious from standard files or easy to rediscover. Do not speculate or document claims you have not verified.
17
+
18
+ If materially different subtrees need scoped instructions, put focused AGENTS.md files in those subtrees instead of expanding the root file. Create them only when they reduce total context and contain genuinely local guidance; do not fragment the instructions unnecessarily or duplicate the root file. Remember that a nested file augments the root instructions for work in that subtree.
19
+
20
+ Make the edits in this invocation, then briefly summarize which AGENTS.md files you created or changed and what you verified.`
21
+
22
+ export function initPrompt(args = '') {
23
+ const request = args.trim()
24
+ return request ? `${INIT_PROMPT}\n\nAdditional request from the user:\n${request}` : INIT_PROMPT
25
+ }
package/src/keys.js ADDED
@@ -0,0 +1,26 @@
1
+ import { setKeys } from '@prsm/ai'
2
+
3
+ const PROVIDERS = [
4
+ { id: 'google', label: 'Gemini', env: ['GEMINI_API_KEY', 'GOOGLE_AI_API_KEY'] },
5
+ { id: 'anthropic', label: 'Claude', env: ['ANTHROPIC_API_KEY'] },
6
+ { id: 'openai', label: 'OpenAI', env: ['OPENAI_API_KEY'] },
7
+ { id: 'xai', label: 'Grok', env: ['XAI_API_KEY'] },
8
+ ]
9
+
10
+ export function discoverKeys(env = process.env) {
11
+ const keys = {}
12
+ for (const p of PROVIDERS) {
13
+ const name = p.env.find((n) => env[n])
14
+ if (name) keys[p.id] = env[name]
15
+ }
16
+ return keys
17
+ }
18
+
19
+ export function applyKeys(keys) {
20
+ setKeys(keys)
21
+ return Object.keys(keys)
22
+ }
23
+
24
+ export function keyHint(providerId) {
25
+ return PROVIDERS.find((p) => p.id === providerId)?.env[0] || `${providerId.toUpperCase()}_API_KEY`
26
+ }
package/src/mcp.js ADDED
@@ -0,0 +1,280 @@
1
+ import { readFile, writeFile } from 'node:fs/promises'
2
+ import { connectMCP } from '@prsm/ai'
3
+ import { globalMcpFile, projectMcpFile, projectDir, ensureDir, picoHome } from './paths.js'
4
+
5
+ function tokenize(str) {
6
+ const tokens = []
7
+ const re = /([A-Za-z0-9_-]+=)?"([^"]*)"|([A-Za-z0-9_-]+=)?'([^']*)'|(\S+)/g
8
+ let m
9
+ while ((m = re.exec(str))) {
10
+ if (m[1] !== undefined || m[3] !== undefined) tokens.push((m[1] ?? m[3]) + (m[2] ?? m[4]))
11
+ else tokens.push(m[2] ?? m[4] ?? m[5])
12
+ }
13
+ return tokens
14
+ }
15
+
16
+ export function parseCommand(str) {
17
+ const tokens = tokenize(str)
18
+
19
+ const env = {}
20
+ while (tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[0])) {
21
+ const sep = tokens[0].indexOf('=')
22
+ env[tokens[0].slice(0, sep)] = tokens[0].slice(sep + 1)
23
+ tokens.shift()
24
+ }
25
+ if (!tokens.length) throw new Error('empty command')
26
+ return { command: tokens[0], args: tokens.slice(1), env }
27
+ }
28
+
29
+ export function parseServerSpec(str) {
30
+ const tokens = tokenize(str)
31
+ if (!/^https?:\/\//.test(tokens[0] || '')) return { type: 'stdio', ...parseCommand(str) }
32
+
33
+ const [url, ...rest] = tokens
34
+ const headers = {}
35
+ for (const token of rest) {
36
+ const sep = token.indexOf('=')
37
+ if (sep < 1) throw new Error(`http server spec only takes a url and Header=value pairs, got "${token}"`)
38
+ headers[token.slice(0, sep)] = token.slice(sep + 1)
39
+ }
40
+ return { type: 'http', url, headers }
41
+ }
42
+
43
+ export const REDACTED_HEADER = '••••••••'
44
+
45
+ export function isSensitiveHeader(name) {
46
+ return /^(authorization|proxy-authorization|cookie|set-cookie|x-api-key|api-key|token|secret)$/i.test(name)
47
+ }
48
+
49
+ function quoteHeaderValue(value) {
50
+ if (!/[\s'"\\]/.test(value)) return value
51
+ if (!value.includes('"')) return `"${value}"`
52
+ if (!value.includes("'")) return `'${value}'`
53
+ throw new Error('header values cannot contain both single and double quotes')
54
+ }
55
+
56
+ export function formatHttpServerSpec(url, headers) {
57
+ const values = Object.entries(headers).map(([name, value]) => `${name}=${quoteHeaderValue(value)}`)
58
+ return [url, ...values].join(' ')
59
+ }
60
+
61
+ export function redactServerSpec(command) {
62
+ let spec
63
+ try {
64
+ spec = parseServerSpec(command)
65
+ } catch {
66
+ return command
67
+ }
68
+ if (spec.type !== 'http') return command
69
+ const headers = Object.fromEntries(Object.entries(spec.headers).map(([name, value]) => [name, isSensitiveHeader(name) ? REDACTED_HEADER : value]))
70
+ return formatHttpServerSpec(spec.url, headers)
71
+ }
72
+
73
+ async function readJson(file, fallback) {
74
+ try {
75
+ return JSON.parse(await readFile(file, 'utf-8'))
76
+ } catch {
77
+ return fallback
78
+ }
79
+ }
80
+
81
+ export function readRegistry() {
82
+ return readJson(globalMcpFile(), { servers: {} })
83
+ }
84
+
85
+ export async function writeRegistry(registry) {
86
+ ensureDir(picoHome())
87
+ await writeFile(globalMcpFile(), JSON.stringify(registry, null, 2) + '\n')
88
+ }
89
+
90
+ export async function readProjectConfig(root) {
91
+ const config = await readJson(projectMcpFile(root), {})
92
+ return { disabled: config.disabled || {}, servers: config.servers || {} }
93
+ }
94
+
95
+ export async function writeProjectConfig(root, config) {
96
+ ensureDir(projectDir(root))
97
+ await writeFile(projectMcpFile(root), JSON.stringify(config, null, 2) + '\n')
98
+ }
99
+
100
+ export async function createMcpTransport(command) {
101
+ const spec = parseServerSpec(command)
102
+ if (spec.type === 'http') {
103
+ const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js')
104
+ const fetch = (url, init) => init?.method === 'GET'
105
+ ? Promise.resolve(new Response(null, { status: 405 }))
106
+ : globalThis.fetch(url, init)
107
+ return new StreamableHTTPClientTransport(new URL(spec.url), {
108
+ requestInit: Object.keys(spec.headers).length ? { headers: spec.headers } : undefined,
109
+ fetch,
110
+ })
111
+ }
112
+ const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js')
113
+ return new StdioClientTransport({
114
+ command: spec.command,
115
+ args: spec.args,
116
+ env: { ...process.env, ...spec.env },
117
+ stderr: 'pipe',
118
+ })
119
+ }
120
+
121
+ export async function createMcpRuntime({ root, onChange = () => {} }) {
122
+ const registry = await readRegistry()
123
+ const projectConfig = await readProjectConfig(root)
124
+ const servers = new Map()
125
+
126
+ const register = (name, command, scope) => {
127
+ servers.set(name, {
128
+ name,
129
+ command,
130
+ scope,
131
+ enabled: !projectConfig.disabled[name],
132
+ status: projectConfig.disabled[name] ? 'disabled' : 'idle',
133
+ error: null,
134
+ connection: null,
135
+ transport: null,
136
+ })
137
+ }
138
+ for (const [name, command] of Object.entries(registry.servers)) register(name, command, 'global')
139
+ for (const [name, command] of Object.entries(projectConfig.servers)) register(name, command, 'project')
140
+
141
+ async function connect(name) {
142
+ const server = servers.get(name)
143
+ if (!server) return
144
+ server.status = 'connecting'
145
+ server.error = null
146
+ onChange()
147
+ try {
148
+ server.transport = await createMcpTransport(server.command)
149
+ server.connection = await connectMCP({
150
+ name,
151
+ transport: () => server.transport,
152
+ })
153
+ server.status = 'connected'
154
+ } catch (err) {
155
+ server.status = 'error'
156
+ server.error = String(err.message || err).slice(0, 300)
157
+ server.connection = null
158
+ server.transport = null
159
+ }
160
+ onChange()
161
+ }
162
+
163
+ async function disconnect(name) {
164
+ const server = servers.get(name)
165
+ if (server?.connection) {
166
+ await server.connection.close().catch(() => {})
167
+ server.connection = null
168
+ }
169
+ }
170
+
171
+ return {
172
+ connectAll() {
173
+ const pending = []
174
+ for (const server of servers.values()) {
175
+ if (server.enabled) pending.push(connect(server.name))
176
+ }
177
+ return Promise.allSettled(pending)
178
+ },
179
+ async add(name, command, scope = 'global') {
180
+ if (scope === 'project') {
181
+ const config = await readProjectConfig(root)
182
+ config.servers[name] = command
183
+ await writeProjectConfig(root, config)
184
+ } else {
185
+ const registry = await readRegistry()
186
+ registry.servers[name] = command
187
+ await writeRegistry(registry)
188
+ }
189
+ servers.set(name, { name, command, scope, enabled: true, status: 'idle', error: null, connection: null, transport: null })
190
+ await connect(name)
191
+ },
192
+ async update(name, command) {
193
+ const server = servers.get(name)
194
+ if (!server) return
195
+ if (server.scope === 'project') {
196
+ const config = await readProjectConfig(root)
197
+ config.servers[name] = command
198
+ await writeProjectConfig(root, config)
199
+ } else {
200
+ const registry = await readRegistry()
201
+ registry.servers[name] = command
202
+ await writeRegistry(registry)
203
+ }
204
+ await disconnect(name)
205
+ server.command = command
206
+ server.status = server.enabled ? 'idle' : 'disabled'
207
+ server.error = null
208
+ onChange()
209
+ if (server.enabled) await connect(name)
210
+ },
211
+ async remove(name) {
212
+ const scope = servers.get(name)?.scope
213
+ if (scope === 'project') {
214
+ const config = await readProjectConfig(root)
215
+ delete config.servers[name]
216
+ await writeProjectConfig(root, config)
217
+ } else {
218
+ const registry = await readRegistry()
219
+ delete registry.servers[name]
220
+ await writeRegistry(registry)
221
+ }
222
+ await disconnect(name)
223
+ servers.delete(name)
224
+ onChange()
225
+ },
226
+ async toggle(name) {
227
+ const server = servers.get(name)
228
+ if (!server) return
229
+ const config = await readProjectConfig(root)
230
+ if (server.enabled) {
231
+ // flip the visible state first: a slow-closing server (headless
232
+ // browsers, heavy processes) must not make disable feel dead
233
+ server.enabled = false
234
+ server.status = 'disabled'
235
+ onChange()
236
+ config.disabled[name] = true
237
+ await writeProjectConfig(root, config)
238
+ disconnect(name).catch(() => {})
239
+ } else {
240
+ server.enabled = true
241
+ delete config.disabled[name]
242
+ await writeProjectConfig(root, config)
243
+ await connect(name)
244
+ }
245
+ },
246
+ reconnect(name) {
247
+ return disconnect(name).then(() => connect(name))
248
+ },
249
+ list() {
250
+ return [...servers.values()].map((s) => ({
251
+ name: s.name,
252
+ command: s.command,
253
+ scope: s.scope,
254
+ enabled: s.enabled,
255
+ status: s.status,
256
+ error: s.error,
257
+ toolCount: s.connection?.tools.length || 0,
258
+ tools: (s.connection?.tools || []).map((t) => ({ name: t.name, description: t.description || '' })),
259
+ }))
260
+ },
261
+ tools() {
262
+ return [...servers.values()]
263
+ .filter((s) => s.connection)
264
+ .flatMap((s) => s.connection.tools)
265
+ },
266
+ closeAll() {
267
+ return Promise.allSettled([...servers.keys()].map(disconnect))
268
+ },
269
+ terminateAll() {
270
+ for (const server of servers.values()) {
271
+ const pid = server.transport?.pid
272
+ if (pid) {
273
+ try {
274
+ process.kill(pid, 'SIGTERM')
275
+ } catch {}
276
+ }
277
+ }
278
+ },
279
+ }
280
+ }
package/src/memory.js ADDED
@@ -0,0 +1,120 @@
1
+ import { readFile, readdir, writeFile, mkdir, unlink, rename, access } from 'node:fs/promises'
2
+ import { basename, dirname, join } from 'node:path'
3
+ import { picoHome, projectDir } from './paths.js'
4
+ import { parseFrontmatter } from './skills.js'
5
+
6
+ export function globalMemoryDir() {
7
+ return join(picoHome(), 'memory')
8
+ }
9
+
10
+ export function projectMemoryDir(root) {
11
+ return join(projectDir(root), 'memory')
12
+ }
13
+
14
+ export function slugify(name) {
15
+ const slug = String(name)
16
+ .toLowerCase()
17
+ .replace(/[^a-z0-9]+/g, '-')
18
+ .replace(/^-+|-+$/g, '')
19
+ .slice(0, 60)
20
+ if (!slug) throw new Error('memory name must contain letters or numbers')
21
+ return slug
22
+ }
23
+
24
+ async function scanDir(dir, scope) {
25
+ let names = []
26
+ try {
27
+ names = await readdir(dir)
28
+ } catch {
29
+ return []
30
+ }
31
+ const memories = []
32
+ for (const file of names) {
33
+ if (!file.endsWith('.md')) continue
34
+ const disabled = file.startsWith('_')
35
+ const filename = disabled ? file.slice(1) : file
36
+ try {
37
+ const { meta, body } = parseFrontmatter(await readFile(join(dir, file), 'utf-8'))
38
+ memories.push({
39
+ name: meta.name || filename.slice(0, -3),
40
+ description: meta.description || '',
41
+ scope,
42
+ file: join(dir, file),
43
+ body: body.trim(),
44
+ disabled,
45
+ })
46
+ } catch {}
47
+ }
48
+ return memories
49
+ }
50
+
51
+ export function createMemory(root) {
52
+ const dirFor = (scope) => (scope === 'global' ? globalMemoryDir() : projectMemoryDir(root))
53
+
54
+ return {
55
+ async list() {
56
+ const global = await scanDir(globalMemoryDir(), 'global')
57
+ const project = await scanDir(projectMemoryDir(root), 'project')
58
+ return [...project, ...global].sort((a, b) =>
59
+ a.name.localeCompare(b.name) || Number(a.disabled) - Number(b.disabled) || a.scope.localeCompare(b.scope),
60
+ )
61
+ },
62
+ async remember({ name, description, content, scope = 'project' }) {
63
+ if (!['project', 'global'].includes(scope)) throw new Error('scope must be project or global')
64
+ const slug = slugify(name)
65
+ const dir = dirFor(scope)
66
+ await mkdir(dir, { recursive: true })
67
+ const file = join(dir, `${slug}.md`)
68
+ const text = `---\nname: ${slug}\ndescription: ${String(description).replace(/\n/g, ' ')}\n---\n${content}\n`
69
+ await writeFile(file, text, 'utf-8')
70
+ return { name: slug, scope, file }
71
+ },
72
+ async forget(target) {
73
+ const memories = await this.list()
74
+ const memory = typeof target === 'string'
75
+ ? memories.find((m) => m.name === target && !m.disabled)
76
+ : memories.find((m) => m.file === target.file)
77
+ if (!memory) throw new Error(`no memory named "${typeof target === 'string' ? target : target.name}"`)
78
+ await unlink(memory.file)
79
+ return { name: memory.name, scope: memory.scope }
80
+ },
81
+ async setDisabled(target, disabled) {
82
+ const memories = await this.list()
83
+ const memory = memories.find((m) => m.file === target.file)
84
+ if (!memory) throw new Error(`no memory named "${target.name}"`)
85
+ if (memory.disabled === disabled) return memory
86
+ const filename = basename(memory.file)
87
+ const destination = join(dirname(memory.file), disabled ? `_${filename}` : filename.slice(1))
88
+ try {
89
+ await access(destination)
90
+ throw new Error(`cannot ${disabled ? 'disable' : 'enable'} "${memory.name}": ${basename(destination)} already exists`)
91
+ } catch (err) {
92
+ if (err.code !== 'ENOENT') throw err
93
+ }
94
+ await rename(memory.file, destination)
95
+ return { ...memory, file: destination, disabled }
96
+ },
97
+ async recall(name) {
98
+ const memories = await this.list()
99
+ const active = memories.filter((m) => !m.disabled)
100
+ const memory = active.find((m) => m.name === name)
101
+ if (!memory) {
102
+ const known = active.map((m) => m.name).join(', ') || 'none'
103
+ throw new Error(`no memory named "${name}"; known memories: ${known}`)
104
+ }
105
+ return { name: memory.name, scope: memory.scope, file: memory.file, content: memory.body }
106
+ },
107
+ }
108
+ }
109
+
110
+ export function memoryIndex(memories, root) {
111
+ const active = memories.filter((m) => !m.disabled)
112
+ if (active.length === 0) {
113
+ return `You have no saved memories yet. This index lists them when you save durable facts with the remember tool; answer questions about your memories from this index alone, without searching the filesystem.`
114
+ }
115
+ const lines = active.map((m) => `- ${m.name} (${m.scope}): ${m.description}`)
116
+ return [
117
+ `Memories you have saved. This index is complete: answer questions about your memories from it directly, load one with the recall tool when its content is relevant, and never search the filesystem for memories. The files live in ${projectMemoryDir(root)} and ${globalMemoryDir()} and can be edited or deleted with ordinary tools when asked to curate them.`,
118
+ ...lines,
119
+ ].join('\n')
120
+ }