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.
- package/README.md +15 -0
- package/package.json +33 -0
- package/src/agent-transcript.js +80 -0
- package/src/agent.js +170 -0
- package/src/agents.js +213 -0
- package/src/attachments.js +141 -0
- package/src/boot.js +28 -0
- package/src/catalog-snapshot.json +1 -0
- package/src/catalog.js +86 -0
- package/src/codex-models.js +56 -0
- package/src/commands.js +61 -0
- package/src/compaction.js +82 -0
- package/src/completion.js +21 -0
- package/src/config.js +32 -0
- package/src/context.js +82 -0
- package/src/controller.js +1263 -0
- package/src/conversation-search.js +101 -0
- package/src/deliberation-history.js +65 -0
- package/src/deliberation.js +61 -0
- package/src/derive.js +307 -0
- package/src/events.js +54 -0
- package/src/export.js +16 -0
- package/src/files.js +39 -0
- package/src/format.js +6 -0
- package/src/fuzzy.js +41 -0
- package/src/git.js +156 -0
- package/src/history.js +51 -0
- package/src/init.js +25 -0
- package/src/keys.js +26 -0
- package/src/mcp.js +280 -0
- package/src/memory.js +120 -0
- package/src/models.js +14 -0
- package/src/openai-auth.js +204 -0
- package/src/paths.js +67 -0
- package/src/reversible-edit.js +79 -0
- package/src/rewind.js +84 -0
- package/src/session-index.js +264 -0
- package/src/session-lock.js +27 -0
- package/src/session.js +160 -0
- package/src/shells.js +166 -0
- package/src/skills.js +164 -0
- package/src/steer.js +129 -0
- package/src/system-prompt.js +49 -0
- package/src/terminal-theme.js +49 -0
- package/src/tools/bash.js +184 -0
- package/src/tools/diff.js +18 -0
- package/src/tools/edit.js +95 -0
- package/src/tools/glob.js +43 -0
- package/src/tools/grep.js +59 -0
- package/src/tools/index.js +296 -0
- package/src/tools/read.js +49 -0
- package/src/tools/recorder.js +74 -0
- package/src/tools/web.js +84 -0
- package/src/tools/write.js +48 -0
- package/src/update.js +82 -0
- package/src/user-tools.js +59 -0
- package/src/wakeups.js +40 -0
package/src/tools/web.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const DEFAULT_SLICE_CHARS = 24000
|
|
2
|
+
const MAX_SLICE_CHARS = 100000
|
|
3
|
+
|
|
4
|
+
export function resolveDredge(config = {}, env = process.env) {
|
|
5
|
+
const url = env.DREDGE_URL || config.dredge?.url || null
|
|
6
|
+
if (!url) return null
|
|
7
|
+
return {
|
|
8
|
+
url: url.replace(/\/+$/, ''),
|
|
9
|
+
apiKey: env.DREDGE_API_KEY || config.dredge?.apiKey || null,
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const CALL_TIMEOUT_MS = 120000
|
|
14
|
+
|
|
15
|
+
async function call(dredge, path, params, signal) {
|
|
16
|
+
const query = new URLSearchParams(params)
|
|
17
|
+
// bounded above dredge's own 90s queue ceiling so a wedged server becomes
|
|
18
|
+
// a tool error instead of a hung turn
|
|
19
|
+
const signals = [AbortSignal.timeout(CALL_TIMEOUT_MS), ...(signal ? [signal] : [])]
|
|
20
|
+
const response = await fetch(`${dredge.url}${path}?${query}`, {
|
|
21
|
+
signal: AbortSignal.any(signals),
|
|
22
|
+
headers: dredge.apiKey ? { authorization: `Bearer ${dredge.apiKey}` } : {},
|
|
23
|
+
})
|
|
24
|
+
const body = await response.json().catch(() => null)
|
|
25
|
+
if (!body) throw new Error(`dredge returned http ${response.status} with no usable body`)
|
|
26
|
+
return body
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createWebTools({ dredge, recorder, signal }) {
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
name: 'web_search',
|
|
33
|
+
description:
|
|
34
|
+
'Search the web. Google-style operators work: site:, filetype:pdf, quoted phrases. Returns ranked results; pass a result url to web_fetch to read it.',
|
|
35
|
+
schema: {
|
|
36
|
+
q: { type: 'string', description: 'the search query' },
|
|
37
|
+
description: { type: 'string', description: 'briefly explain what this search is intended to find, shown to the human watching' },
|
|
38
|
+
},
|
|
39
|
+
execute: async ({ q }) => {
|
|
40
|
+
recorder.extra({ title: q })
|
|
41
|
+
const body = await call(dredge, '/search', { q }, signal)
|
|
42
|
+
if (!body.ok) throw new Error(body.error?.message || 'search failed')
|
|
43
|
+
const results = (body.results || []).map((r) => ({
|
|
44
|
+
title: r.title,
|
|
45
|
+
url: r.url,
|
|
46
|
+
snippet: r.snippet,
|
|
47
|
+
source: r.source,
|
|
48
|
+
}))
|
|
49
|
+
if (results.length === 0) {
|
|
50
|
+
const backends = (body.backends || []).map((b) => `${b.name}: ${b.status}`).join(', ')
|
|
51
|
+
return { results, note: backends ? `no results · backends: ${backends}` : 'no results' }
|
|
52
|
+
}
|
|
53
|
+
return { results }
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'web_fetch',
|
|
58
|
+
description:
|
|
59
|
+
'Fetch a url and read it as clean markdown (html, pdf, docx, and textual formats like json). Long documents arrive in slices: the result says which slice you have (e.g. "slice 1 of 12") and next_cursor continues from there. Every slice you fetch permanently occupies conversation context, so only walk cursors for content you actually need, and raise maxChars only when the task genuinely needs a bigger window.',
|
|
60
|
+
schema: {
|
|
61
|
+
url: { type: 'string', description: 'the url to fetch' },
|
|
62
|
+
cursor: { type: 'string', description: 'pagination cursor from a previous web_fetch of the same url', optional: true },
|
|
63
|
+
maxChars: { type: 'number', description: `slice size in characters, default ${DEFAULT_SLICE_CHARS}, max ${MAX_SLICE_CHARS}`, optional: true },
|
|
64
|
+
},
|
|
65
|
+
execute: async ({ url, cursor, maxChars }) => {
|
|
66
|
+
recorder.extra({ title: url.replace(/^https?:\/\//, '').slice(0, 80) })
|
|
67
|
+
const slice = Math.min(MAX_SLICE_CHARS, Math.max(1000, maxChars || DEFAULT_SLICE_CHARS))
|
|
68
|
+
const body = await call(dredge, '/fetch', { url, maxChars: slice, ...(cursor && { cursor }) }, signal)
|
|
69
|
+
if (!body.ok) {
|
|
70
|
+
const { code, message, retryable } = body.error || {}
|
|
71
|
+
throw new Error(`${code || 'fetch failed'}: ${message || url}${retryable ? ' (retryable)' : ''}`)
|
|
72
|
+
}
|
|
73
|
+
const { markdown, metadata, pagination } = body.doc
|
|
74
|
+
return {
|
|
75
|
+
markdown,
|
|
76
|
+
title: metadata?.title || null,
|
|
77
|
+
finalUrl: metadata?.final_url || url,
|
|
78
|
+
...(pagination?.total_chunks > 1 && { slice: `${pagination.chunk_index + 1} of ${pagination.total_chunks}` }),
|
|
79
|
+
...(pagination?.next_cursor && { next_cursor: pagination.next_cursor }),
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
]
|
|
84
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { dirname, resolve } from 'node:path'
|
|
3
|
+
import { makeWriteEdit } from '../reversible-edit.js'
|
|
4
|
+
import { makeDiff } from './diff.js'
|
|
5
|
+
|
|
6
|
+
export function createWrite({ cwd, recorder, tracker }) {
|
|
7
|
+
return {
|
|
8
|
+
name: 'write',
|
|
9
|
+
description: 'Write content to a file, creating it and any parent directories if needed. Overwrites existing content.',
|
|
10
|
+
schema: {
|
|
11
|
+
description: { type: 'string', description: 'briefly explain why this tool call is needed, shown to the human watching' },
|
|
12
|
+
path: { type: 'string', description: 'file path, relative to the working directory or absolute' },
|
|
13
|
+
content: { type: 'string', description: 'the full file content to write' },
|
|
14
|
+
},
|
|
15
|
+
execute: async ({ path, content }) => {
|
|
16
|
+
const full = resolve(cwd, path)
|
|
17
|
+
recorder.extra({ title: path })
|
|
18
|
+
await mkdir(dirname(full), { recursive: true })
|
|
19
|
+
let before = null
|
|
20
|
+
try {
|
|
21
|
+
before = await readFile(full, 'utf-8')
|
|
22
|
+
} catch (err) {
|
|
23
|
+
if (err.code !== 'ENOENT') throw err
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (before === content) {
|
|
27
|
+
return { ok: true, path, unchanged: true, note: 'file already had exactly this content' }
|
|
28
|
+
}
|
|
29
|
+
await writeFile(full, content, 'utf-8')
|
|
30
|
+
|
|
31
|
+
const result = { ok: true, path }
|
|
32
|
+
if (before !== null) {
|
|
33
|
+
const diff = makeDiff(path, before, content)
|
|
34
|
+
recorder.extra({ diff, revert: makeWriteEdit(full, before, content, true) })
|
|
35
|
+
result.additions = diff.additions
|
|
36
|
+
result.deletions = diff.deletions
|
|
37
|
+
} else {
|
|
38
|
+
const diff = makeDiff(path, '', content)
|
|
39
|
+
recorder.extra({ diff, revert: makeWriteEdit(full, '', content, false), created: true })
|
|
40
|
+
result.additions = diff.additions
|
|
41
|
+
result.created = true
|
|
42
|
+
}
|
|
43
|
+
const context = tracker.check(full)
|
|
44
|
+
if (context.length) result.context_from_agents_md = context
|
|
45
|
+
return result
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/update.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { exec } from 'node:child_process'
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { picoHome, ensureDir } from './paths.js'
|
|
5
|
+
|
|
6
|
+
const CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000
|
|
7
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/picocode/latest'
|
|
8
|
+
|
|
9
|
+
function stateFile() {
|
|
10
|
+
return join(picoHome(), 'update-check.json')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function readState() {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(await readFile(stateFile(), 'utf-8'))
|
|
16
|
+
} catch {
|
|
17
|
+
return {}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function writeState(patch) {
|
|
22
|
+
const state = { ...(await readState()), ...patch }
|
|
23
|
+
ensureDir(picoHome())
|
|
24
|
+
await writeFile(stateFile(), JSON.stringify(state) + '\n')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function fetchLatestVersion({ timeoutMs = 5000 } = {}) {
|
|
28
|
+
if (process.env.PICO_FAKE_LATEST) return process.env.PICO_FAKE_LATEST
|
|
29
|
+
const response = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(timeoutMs) })
|
|
30
|
+
const body = await response.json()
|
|
31
|
+
return body.version || null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function newerVersion(current, latest) {
|
|
35
|
+
if (!latest || latest === current) return null
|
|
36
|
+
const parse = (v) => String(v).split('.').map((n) => parseInt(n, 10) || 0)
|
|
37
|
+
const cur = parse(current)
|
|
38
|
+
const next = parse(latest)
|
|
39
|
+
for (let i = 0; i < 3; i++) {
|
|
40
|
+
if ((next[i] ?? 0) > (cur[i] ?? 0)) return latest
|
|
41
|
+
if ((next[i] ?? 0) < (cur[i] ?? 0)) return null
|
|
42
|
+
}
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// at most one registry hit per interval, one notification per version, all
|
|
47
|
+
// failures silent: an update check must never cost the session anything.
|
|
48
|
+
// PICO_FAKE_LATEST bypasses cadence and memory so the toast can be demoed
|
|
49
|
+
export async function checkForUpdate(currentVersion) {
|
|
50
|
+
const fake = !!process.env.PICO_FAKE_LATEST
|
|
51
|
+
const state = await readState()
|
|
52
|
+
if (!fake && state.lastCheck && Date.now() - state.lastCheck < CHECK_INTERVAL_MS) return null
|
|
53
|
+
|
|
54
|
+
let latest = null
|
|
55
|
+
try {
|
|
56
|
+
latest = await fetchLatestVersion()
|
|
57
|
+
} catch {
|
|
58
|
+
return null
|
|
59
|
+
} finally {
|
|
60
|
+
if (!fake) await writeState({ lastCheck: Date.now() }).catch(() => {})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const newer = newerVersion(currentVersion, latest)
|
|
64
|
+
if (!newer) return null
|
|
65
|
+
if (!fake && state.notifiedVersion === newer) return null
|
|
66
|
+
return {
|
|
67
|
+
version: newer,
|
|
68
|
+
markNotified: () => (fake ? Promise.resolve() : writeState({ notifiedVersion: newer }).catch(() => {})),
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isDevInstall(entryUrl) {
|
|
73
|
+
return !String(entryUrl).includes('/node_modules/')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function runUpdate() {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
exec('npm install -g picocode@latest', { timeout: 180000 }, (err, stdout, stderr) => {
|
|
79
|
+
resolve({ ok: !err, output: [stdout, stderr].filter(Boolean).join('\n').trim().slice(0, 400) })
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { readdir, stat } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
import { picoHome } from './paths.js'
|
|
5
|
+
|
|
6
|
+
export function globalToolsDir() {
|
|
7
|
+
return join(picoHome(), 'tools')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function projectToolsDir(root) {
|
|
11
|
+
return join(root, '.pico', 'tools')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function validate(tool, file) {
|
|
15
|
+
if (!tool || typeof tool !== 'object') throw new Error('default export is not a tool object')
|
|
16
|
+
if (typeof tool.name !== 'string' || !tool.name) throw new Error('tool.name must be a string')
|
|
17
|
+
if (typeof tool.description !== 'string') throw new Error('tool.description must be a string')
|
|
18
|
+
if (typeof tool.execute !== 'function') throw new Error('tool.execute must be a function')
|
|
19
|
+
if (!tool.schema || typeof tool.schema !== 'object') throw new Error('tool.schema must be an object')
|
|
20
|
+
return { ...tool, _file: file }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function loadTool(file, context) {
|
|
24
|
+
const { mtimeMs } = await stat(file)
|
|
25
|
+
const module = await import(`${pathToFileURL(file).href}?v=${mtimeMs}`)
|
|
26
|
+
const exported = module.default
|
|
27
|
+
const tool = typeof exported === 'function' ? await exported(context) : exported
|
|
28
|
+
return validate(tool, file)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function scanDir(dir, context, source) {
|
|
32
|
+
let names = []
|
|
33
|
+
try {
|
|
34
|
+
names = await readdir(dir)
|
|
35
|
+
} catch {
|
|
36
|
+
return { tools: [], errors: [] }
|
|
37
|
+
}
|
|
38
|
+
const tools = []
|
|
39
|
+
const errors = []
|
|
40
|
+
for (const name of names) {
|
|
41
|
+
if (!name.endsWith('.js') && !name.endsWith('.mjs')) continue
|
|
42
|
+
const file = join(dir, name)
|
|
43
|
+
try {
|
|
44
|
+
tools.push({ ...(await loadTool(file, context)), source })
|
|
45
|
+
} catch (err) {
|
|
46
|
+
errors.push({ file, error: String(err.message || err).slice(0, 200) })
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { tools, errors }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function scanUserTools({ cwd, root }) {
|
|
53
|
+
const context = { cwd, root }
|
|
54
|
+
const global = await scanDir(globalToolsDir(), context, 'global')
|
|
55
|
+
const project = await scanDir(projectToolsDir(root), context, 'project')
|
|
56
|
+
const byName = new Map()
|
|
57
|
+
for (const tool of [...global.tools, ...project.tools]) byName.set(tool.name, tool)
|
|
58
|
+
return { tools: [...byName.values()], errors: [...global.errors, ...project.errors] }
|
|
59
|
+
}
|
package/src/wakeups.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function createWakeupManager({ onFire = () => {}, onChange = () => {} } = {}) {
|
|
2
|
+
const wakeups = new Map()
|
|
3
|
+
let nextId = 1
|
|
4
|
+
|
|
5
|
+
return {
|
|
6
|
+
schedule(delaySeconds, note) {
|
|
7
|
+
const seconds = Math.max(5, Math.round(delaySeconds))
|
|
8
|
+
const id = String(nextId++)
|
|
9
|
+
const at = Date.now() + seconds * 1000
|
|
10
|
+
const timer = setTimeout(() => {
|
|
11
|
+
wakeups.delete(id)
|
|
12
|
+
onChange()
|
|
13
|
+
onFire({ id, note, at })
|
|
14
|
+
}, seconds * 1000)
|
|
15
|
+
wakeups.set(id, { id, note, at, timer })
|
|
16
|
+
onChange()
|
|
17
|
+
return { id, at, seconds }
|
|
18
|
+
},
|
|
19
|
+
cancel(id) {
|
|
20
|
+
const wakeup = wakeups.get(String(id))
|
|
21
|
+
if (!wakeup) throw new Error(`no pending wake-up with id ${id}`)
|
|
22
|
+
clearTimeout(wakeup.timer)
|
|
23
|
+
wakeups.delete(String(id))
|
|
24
|
+
onChange()
|
|
25
|
+
return { id: wakeup.id, cancelled: true }
|
|
26
|
+
},
|
|
27
|
+
list() {
|
|
28
|
+
return [...wakeups.values()]
|
|
29
|
+
.map(({ id, note, at }) => ({ id, note, at }))
|
|
30
|
+
.sort((a, b) => a.at - b.at)
|
|
31
|
+
},
|
|
32
|
+
pending() {
|
|
33
|
+
return wakeups.size
|
|
34
|
+
},
|
|
35
|
+
cancelAll() {
|
|
36
|
+
for (const wakeup of wakeups.values()) clearTimeout(wakeup.timer)
|
|
37
|
+
wakeups.clear()
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
}
|