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
@@ -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
+ }