picocode-core 0.9.134 → 0.9.136

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "picocode-core",
3
- "version": "0.9.134",
3
+ "version": "0.9.136",
4
4
  "description": "The agent runtime behind pico: sessions, tools, subagents, MCP, memory, and model access",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/commands.js CHANGED
@@ -3,6 +3,8 @@ import { join } from 'node:path'
3
3
  import { picoHome } from './paths.js'
4
4
  import { parseFrontmatter } from './skills.js'
5
5
 
6
+ const ARGUMENT = /\{\{([A-Za-z][A-Za-z0-9_-]*):(path|text|choice\(([^{}()]*)\))(?:\:([^{}]*))?\}\}/g
7
+
6
8
  export function globalCommandsDir() {
7
9
  return join(picoHome(), 'commands')
8
10
  }
@@ -11,6 +13,31 @@ export function projectCommandsDir(root) {
11
13
  return join(root, '.pico', 'commands')
12
14
  }
13
15
 
16
+ function inferredLabel(name) {
17
+ const words = name.replace(/[-_]+/g, ' ').trim()
18
+ return words ? words[0].toUpperCase() + words.slice(1) : name
19
+ }
20
+
21
+ export function parseCommandArguments(body) {
22
+ const arguments_ = []
23
+ const seen = new Set()
24
+ for (const match of body.matchAll(ARGUMENT)) {
25
+ const [, name, typeSpec, choicesText, explicitLabel] = match
26
+ if (seen.has(name)) continue
27
+ const type = typeSpec.startsWith('choice(') ? 'choice' : typeSpec
28
+ const choices = type === 'choice' ? choicesText.split(',').map((v) => v.trim()).filter(Boolean) : undefined
29
+ if (type === 'choice' && choices.length === 0) continue
30
+ seen.add(name)
31
+ arguments_.push({
32
+ name,
33
+ type,
34
+ label: explicitLabel?.trim() || inferredLabel(name),
35
+ ...(choices ? { choices } : {}),
36
+ })
37
+ }
38
+ return arguments_
39
+ }
40
+
14
41
  async function scanDir(dir, source) {
15
42
  let names = []
16
43
  try {
@@ -22,24 +49,42 @@ async function scanDir(dir, source) {
22
49
  for (const file of names) {
23
50
  if (!file.endsWith('.md')) continue
24
51
  try {
25
- const { meta } = parseFrontmatter(await readFile(join(dir, file), 'utf-8'))
52
+ const text = await readFile(join(dir, file), 'utf-8')
53
+ const { meta, body } = parseFrontmatter(text)
26
54
  commands.push({
27
55
  name: file.slice(0, -3),
28
56
  description: meta.description || '',
29
57
  source,
30
58
  file: join(dir, file),
59
+ arguments: parseCommandArguments(body),
31
60
  })
32
61
  } catch {}
33
62
  }
34
63
  return commands
35
64
  }
36
65
 
37
- export function expandCommand(body, args) {
38
- return body.includes('$ARGUMENTS')
39
- ? body.replaceAll('$ARGUMENTS', args)
66
+ export function expandCommand(body, args = '', namedValues = {}) {
67
+ if (args && typeof args === 'object' && !Array.isArray(args)) {
68
+ namedValues = args
69
+ args = ''
70
+ }
71
+ args = String(args ?? '')
72
+ namedValues = namedValues && typeof namedValues === 'object' ? namedValues : {}
73
+ const metadata = new Map(parseCommandArguments(body).map((argument) => [argument.name, argument]))
74
+ const rendered = body.replace(ARGUMENT, (markup, name) => {
75
+ const argument = metadata.get(name)
76
+ if (!argument) return markup
77
+ const value = Object.hasOwn(namedValues, name) ? String(namedValues[name] ?? '') : ''
78
+ if (argument.type === 'choice' && value && !argument.choices.includes(value)) {
79
+ throw new TypeError(`Invalid value for command argument "${name}"`)
80
+ }
81
+ return value
82
+ })
83
+ return rendered.includes('$ARGUMENTS')
84
+ ? rendered.replaceAll('$ARGUMENTS', () => args)
40
85
  : args
41
- ? `${body.trim()}\n\n${args}`
42
- : body
86
+ ? `${rendered.trim()}\n\n${args}`
87
+ : rendered
43
88
  }
44
89
 
45
90
  export async function createCommandIndex(root) {
@@ -51,11 +96,11 @@ export async function createCommandIndex(root) {
51
96
 
52
97
  return {
53
98
  list: () => commands,
54
- async load(name, args = '') {
99
+ async load(name, args = '', namedValues = {}) {
55
100
  const command = byName.get(name)
56
101
  if (!command) return null
57
102
  const { body } = parseFrontmatter(await readFile(command.file, 'utf-8'))
58
- return expandCommand(body.trim(), args.trim())
103
+ return expandCommand(body.trim(), args, namedValues)
59
104
  },
60
105
  }
61
106
  }
package/src/controller.js CHANGED
@@ -28,7 +28,8 @@ import { connectOpenAI, openaiCredentials, disconnectOpenAI } from './openai-aut
28
28
  import { agentScratchDir, ensureDir } from './paths.js'
29
29
  import { loadCodexModels } from './codex-models.js'
30
30
  import { fuzzyScore } from './fuzzy.js'
31
- import { finalizeUserContent, inputTextFromContent, mediaTypeFor } from './attachments.js'
31
+ import { MAX_DELIBERATION_ROUNDS } from './deliberation.js'
32
+ import { buildUserContent, finalizeUserContent, inputTextFromContent, mediaTypeFor } from './attachments.js'
32
33
 
33
34
  export const EFFORT_LEVELS = [
34
35
  { key: null, desc: 'let the provider decide how much to think' },
@@ -73,8 +74,9 @@ function parallelPrompt(task, agentLimit) {
73
74
  return `Use parallel agents for the following task: ${task}\n\nFirst call agent_plan. Interpret any agent-count instruction in the user's task semantically and declare that count; if the user gave no count, declare the configured default budget of ${agentLimit}. Then use agent_start within that enforced budget to delegate distinct, focused parts of the task to the configured worker model. Collect workers with agent_collect, critically evaluate their results, and synthesize the final response. When useful and the budget permits, use independent workers to check important disputed or weak conclusions. Do not delegate final synthesis. Do not emit progress updates while agents run; Pico displays agent activity automatically.`
74
75
  }
75
76
 
76
- function deliberatePrompt(decision) {
77
- return `Deliberate on the following decision: ${decision}\n\nImmediately call deliberate with a self-contained brief. Do not research first, start ordinary agents, or approximate the deliberation yourself. The deliberation participants own all supporting research.`
77
+ function deliberatePrompt(decision, rounds) {
78
+ const withRounds = rounds ? ` and rounds set to ${rounds}` : ''
79
+ return `Deliberate on the following decision: ${decision}\n\nImmediately call deliberate with a self-contained brief${withRounds}. Do not research first, start ordinary agents, or approximate the deliberation yourself. The deliberation participants own all supporting research.`
78
80
  }
79
81
 
80
82
  function workerSystemPrompt(scratchpad) {
@@ -364,8 +366,11 @@ export function createController({ boot }) {
364
366
  }
365
367
 
366
368
  async function executeTurn(text) {
367
- const { content, used } = finalizeUserContent(text, state.attachments)
368
- const viewed = used.length > 0 && used.every((placeholder) => deliveredImages.has(placeholder))
369
+ // an image the model asked to view arrives with its path in the label;
370
+ // that text must not be scanned for image paths or it attaches twice
371
+ const built = buildUserContent(text, state.attachments)
372
+ const viewed = built.used.length > 0 && built.used.every((placeholder) => deliveredImages.has(placeholder))
373
+ const { content, used } = viewed ? built : finalizeUserContent(text, state.attachments)
369
374
  for (const placeholder of used) deliveredImages.delete(placeholder)
370
375
  persist(makeEvent('message', { message: { role: 'user', content }, ...(viewed ? { origin: 'view' } : {}) }))
371
376
  ensureSession()
@@ -982,13 +987,16 @@ export function createController({ boot }) {
982
987
  return true
983
988
  }
984
989
 
985
- function sendDeliberate(decision) {
990
+ // rounds, when given, is passed along to the tool call; the tool itself
991
+ // clamps to its 1..5 range
992
+ function sendDeliberate(decision, rounds) {
986
993
  if (!decision) {
987
994
  flash('usage: /deliberate <decision>')
988
995
  return true
989
996
  }
990
997
  if (!modelAvailable(boot.deliberationModel)) return false
991
- send(deliberatePrompt(decision))
998
+ const n = Number(rounds)
999
+ send(deliberatePrompt(decision, Number.isInteger(n) && n >= 1 && n <= MAX_DELIBERATION_ROUNDS ? n : null))
992
1000
  return true
993
1001
  }
994
1002
 
@@ -999,8 +1007,8 @@ export function createController({ boot }) {
999
1007
  send(`Follow these skill instructions now.\n\n${body}`)
1000
1008
  }
1001
1009
 
1002
- async function sendCommand(name, args) {
1003
- const text = await boot.commands.load(name, args)
1010
+ async function sendCommand(name, args, namedValues) {
1011
+ const text = await boot.commands.load(name, args, namedValues)
1004
1012
  if (!text) return flash(`could not load command ${name}`)
1005
1013
  send(text)
1006
1014
  }
package/src/skills.js CHANGED
@@ -121,8 +121,20 @@ Review $ARGUMENTS for security problems. Focus on input validation and secrets.
121
121
  \`\`\`
122
122
 
123
123
  \`$ARGUMENTS\` is replaced with whatever follows the command; without the placeholder,
124
- arguments are appended after the body. Commands are rescanned every turn, so a new
125
- command appears in the slash menu from the next message.`
124
+ arguments are appended after the body. For compact typed form fields, put named
125
+ placeholders directly in the Markdown body:
126
+
127
+ - \`{{file:path:File to review}}\` — path/file picker, named \`file\`
128
+ - \`{{focus:text:Review focus}}\` — text input, named \`focus\`
129
+ - \`{{mode:choice(review,fix):Mode}}\` — choice picker whose selected literal is inserted
130
+
131
+ The final label is optional and inferred from the name (for example, \`{{focus:text}}\`
132
+ is labelled "Focus"). Names start with a letter and may contain letters, digits, \`_\`,
133
+ and \`-\`. Keep choice values short and comma-separated. A placeholder may be repeated.
134
+ Named fields and \`$ARGUMENTS\` may coexist for backwards compatibility.
135
+
136
+ Commands are rescanned every turn, so a new command appears in the slash menu from the
137
+ next message.`
126
138
 
127
139
  export async function createSkillIndex(root) {
128
140
  const builtin = [