zooid 0.11.2 → 0.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/README.md +6 -3
- package/dist/bin.js +157 -14
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-KGYQ5YNP.js → chunk-3Q4BPAZD.js} +23 -2
- package/dist/chunk-3Q4BPAZD.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +8 -8
- package/src/bin.ts +3 -3
- package/src/build-registry.ts +1 -1
- package/src/build-registry.zod044.test.ts +23 -0
- package/src/commands/init/generators.test.ts +43 -0
- package/src/commands/init/generators.ts +24 -5
- package/src/commands/init/pi-scaffold.test.ts +151 -0
- package/src/commands/init/prompts.ts +53 -2
- package/src/commands/init/registry.test.ts +60 -0
- package/src/commands/init/registry.ts +58 -0
- package/src/commands/init/sniff.test.ts +61 -2
- package/src/commands/init/sniff.ts +43 -12
- package/src/commands/init.ts +91 -5
- package/dist/chunk-KGYQ5YNP.js.map +0 -1
|
@@ -1,30 +1,61 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { homedir } from 'node:os'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
|
-
import { findSimplePreset } from './registry.js'
|
|
4
|
+
import { findSimplePreset, PI_AUTH_FILE, PI_SETTINGS_FILE } from './registry.js'
|
|
5
5
|
|
|
6
6
|
export interface SniffResult {
|
|
7
7
|
found: boolean
|
|
8
|
-
/** Absolute path that satisfied the sniff (the config directory). */
|
|
8
|
+
/** Absolute path that satisfied the sniff (the config directory, or file for pi). */
|
|
9
9
|
path?: string
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Checks whether the operator's per-CLI config directory
|
|
14
|
-
* any file inside — only `existsSync
|
|
15
|
-
* macOS Keychain, and OS keyring without per-backend
|
|
16
|
-
* where the dir exists but the operator isn't actually
|
|
17
|
-
* the spawned shim will surface a clearer error on
|
|
13
|
+
* Checks whether the operator's per-CLI config directory (or, for pi, the
|
|
14
|
+
* credential file) exists. NEVER reads any file inside — only `existsSync`.
|
|
15
|
+
* One rule covers Linux, macOS Keychain, and OS keyring without per-backend
|
|
16
|
+
* logic. The edge case where the dir exists but the operator isn't actually
|
|
17
|
+
* logged in is accepted; the spawned shim will surface a clearer error on
|
|
18
|
+
* first turn.
|
|
18
19
|
*
|
|
19
20
|
* `home` is overridable for tests.
|
|
20
21
|
*/
|
|
21
22
|
export function sniffCredentials(
|
|
22
|
-
preset: 'claude' | 'codex',
|
|
23
|
+
preset: 'claude' | 'codex' | 'pi',
|
|
23
24
|
home: string = homedir(),
|
|
24
25
|
): SniffResult {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
26
|
+
// pi's credential is a file (auth.json), not a directory; existsSync covers
|
|
27
|
+
// both, so the only difference is which relative path we join.
|
|
28
|
+
const rel = preset === 'pi' ? PI_AUTH_FILE : findSimplePreset(preset)?.credentialDir
|
|
29
|
+
if (!rel) return { found: false }
|
|
30
|
+
const full = join(home, rel)
|
|
28
31
|
if (existsSync(full)) return { found: true, path: full }
|
|
29
32
|
return { found: false }
|
|
30
33
|
}
|
|
34
|
+
|
|
35
|
+
export interface PiDefaults {
|
|
36
|
+
provider: string
|
|
37
|
+
model: string
|
|
38
|
+
/** Absolute path the pair came from, so the wizard can say where. */
|
|
39
|
+
source: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Reads the operator's global pi defaults. A pair they already run
|
|
44
|
+
* interactively is verified by use, which beats any pin this repo could guess.
|
|
45
|
+
*
|
|
46
|
+
* Returns undefined unless BOTH keys are present — half a pair cannot boot pi.
|
|
47
|
+
* Never throws: a corrupt personal config must not abort onboarding.
|
|
48
|
+
*/
|
|
49
|
+
export function sniffPiDefaults(home: string = homedir()): PiDefaults | undefined {
|
|
50
|
+
const source = join(home, PI_SETTINGS_FILE)
|
|
51
|
+
try {
|
|
52
|
+
const raw = JSON.parse(readFileSync(source, 'utf8')) as Record<string, unknown>
|
|
53
|
+
const provider = raw.defaultProvider
|
|
54
|
+
const model = raw.defaultModel
|
|
55
|
+
if (typeof provider !== 'string' || !provider) return undefined
|
|
56
|
+
if (typeof model !== 'string' || !model) return undefined
|
|
57
|
+
return { provider, model, source }
|
|
58
|
+
} catch {
|
|
59
|
+
return undefined
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/commands/init.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs'
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, symlinkSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
2
3
|
import { dirname, join, resolve } from 'node:path'
|
|
3
4
|
import {
|
|
4
5
|
generateAgentsMd,
|
|
@@ -8,22 +9,28 @@ import {
|
|
|
8
9
|
generateGitignore,
|
|
9
10
|
generateOpencodeJson,
|
|
10
11
|
generateOpencodeReadme,
|
|
12
|
+
generatePiSettings,
|
|
11
13
|
generateZooidYaml,
|
|
12
14
|
} from './init/generators.js'
|
|
13
15
|
import {
|
|
14
16
|
findOpencodeProvider,
|
|
17
|
+
findPiProvider,
|
|
15
18
|
findSimplePreset,
|
|
19
|
+
PI_AGENT_DIR,
|
|
20
|
+
PI_AUTH_FILE,
|
|
21
|
+
PI_DEFAULT_MODEL,
|
|
22
|
+
PI_DEFAULT_PROVIDER,
|
|
16
23
|
} from './init/registry.js'
|
|
17
|
-
import { sniffCredentials } from './init/sniff.js'
|
|
24
|
+
import { sniffCredentials, sniffPiDefaults } from './init/sniff.js'
|
|
18
25
|
|
|
19
26
|
export interface InitOptions {
|
|
20
27
|
dir: string
|
|
21
|
-
preset: 'claude' | 'codex' | 'opencode'
|
|
22
|
-
/** Required for claude/codex; ignored for opencode. */
|
|
28
|
+
preset: 'claude' | 'codex' | 'opencode' | 'pi'
|
|
29
|
+
/** Required for claude/codex/pi; ignored for opencode. */
|
|
23
30
|
auth?: 'subscription' | 'api-key'
|
|
24
31
|
/** Optional model pin. Omitted by default — the harness uses its own default. */
|
|
25
32
|
model?: string
|
|
26
|
-
/** opencode provider id; defaults to `opencode-go`
|
|
33
|
+
/** opencode/pi provider id; defaults to `opencode-go` for opencode. */
|
|
27
34
|
provider?: string
|
|
28
35
|
/** Required on api-key path; required for opencode. */
|
|
29
36
|
apiKey?: string
|
|
@@ -31,6 +38,8 @@ export interface InitOptions {
|
|
|
31
38
|
force?: boolean
|
|
32
39
|
/** Required with `force` to overwrite existing files. */
|
|
33
40
|
overwrite?: boolean
|
|
41
|
+
/** Overridable for tests; defaults to os.homedir(). */
|
|
42
|
+
home?: string
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
interface WriteSpec {
|
|
@@ -66,6 +75,7 @@ export async function runInit(opts: InitOptions): Promise<void> {
|
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
const writes: WriteSpec[] = []
|
|
78
|
+
let piInherited: { provider: string; model: string; source: string } | undefined
|
|
69
79
|
|
|
70
80
|
if (opts.preset === 'claude' || opts.preset === 'codex') {
|
|
71
81
|
if (!opts.auth) throw new Error('--auth (subscription|api-key) is required for claude/codex')
|
|
@@ -123,6 +133,47 @@ export async function runInit(opts: InitOptions): Promise<void> {
|
|
|
123
133
|
content: generateEnv({ envVar: providerMeta!.apiKeyEnvVar, value: opts.apiKey! }),
|
|
124
134
|
})
|
|
125
135
|
}
|
|
136
|
+
} else if (opts.preset === 'pi') {
|
|
137
|
+
// Same binary as claude/codex, because pi has the same two tiers. The
|
|
138
|
+
// difference is that the api-key side also needs a provider.
|
|
139
|
+
if (!opts.auth) throw new Error('--auth (subscription|api-key) is required for pi')
|
|
140
|
+
if (opts.auth === 'subscription' && !sniffCredentials('pi', opts.home).found) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
'no pi login found — run `pi` and /login first, or use --auth api-key',
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
if (opts.auth === 'api-key' && !opts.provider) {
|
|
146
|
+
throw new Error('--provider is required for pi --auth api-key')
|
|
147
|
+
}
|
|
148
|
+
const providerMeta = opts.provider ? findPiProvider(opts.provider) : undefined
|
|
149
|
+
if (opts.provider && !providerMeta) throw new Error(`unknown pi provider: ${opts.provider}`)
|
|
150
|
+
|
|
151
|
+
// Inherit before pinning: a pair the operator already runs interactively is
|
|
152
|
+
// verified by use. Explicit flags still win over an inherited value.
|
|
153
|
+
const inherited = sniffPiDefaults(opts.home)
|
|
154
|
+
const provider = opts.provider ?? inherited?.provider ?? PI_DEFAULT_PROVIDER
|
|
155
|
+
const model = opts.model ?? inherited?.model ?? PI_DEFAULT_MODEL
|
|
156
|
+
const settingsProvider = opts.model ? provider : (inherited?.provider ?? provider)
|
|
157
|
+
|
|
158
|
+
writes.push({ path: 'zooid.yaml', content: generateZooidYaml({ preset: 'pi' }) })
|
|
159
|
+
writes.push({ path: 'agents/zooid-assistant/AGENTS.md', content: generateAgentsMd() })
|
|
160
|
+
writes.push({
|
|
161
|
+
path: `agents/zooid-assistant/${PI_AGENT_DIR}/settings.json`,
|
|
162
|
+
content: generatePiSettings({ provider: settingsProvider, model }),
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// PI_CODING_AGENT_DIR is relative on purpose: the agent's cwd is
|
|
166
|
+
// agents/<name> locally and /workspace in a container, and those are the
|
|
167
|
+
// same directory, so one value is correct under both runtimes (spike 1.1).
|
|
168
|
+
const envLines = [`PI_CODING_AGENT_DIR=${PI_AGENT_DIR}`]
|
|
169
|
+
// Only the api-key path writes a key. The subscription path shares the
|
|
170
|
+
// operator's auth.json instead (below) and writes no credential here.
|
|
171
|
+
if (opts.auth === 'api-key' && opts.apiKey) {
|
|
172
|
+
envLines.push(`${providerMeta!.apiKeyEnvVar}=${opts.apiKey}`)
|
|
173
|
+
}
|
|
174
|
+
writes.push({ path: '.env', content: envLines.join('\n') + '\n' })
|
|
175
|
+
|
|
176
|
+
piInherited = inherited
|
|
126
177
|
} else {
|
|
127
178
|
throw new Error(`unknown preset: ${String(opts.preset)}`)
|
|
128
179
|
}
|
|
@@ -152,5 +203,40 @@ export async function runInit(opts: InitOptions): Promise<void> {
|
|
|
152
203
|
}
|
|
153
204
|
}
|
|
154
205
|
|
|
206
|
+
if (opts.preset === 'pi') {
|
|
207
|
+
if (piInherited) {
|
|
208
|
+
console.log(
|
|
209
|
+
`✓ Inherited defaultProvider=${piInherited.provider} defaultModel=${piInherited.model} from ${piInherited.source}`,
|
|
210
|
+
)
|
|
211
|
+
} else {
|
|
212
|
+
console.log(
|
|
213
|
+
`✓ Pinned defaultProvider=${PI_DEFAULT_PROVIDER} defaultModel=${PI_DEFAULT_MODEL} (pi's own default returns an empty turn)`,
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
const s = sniffCredentials('pi', opts.home)
|
|
217
|
+
if (opts.auth === 'subscription' && s.found) {
|
|
218
|
+
// Symlink, not copy: pi rotates OAuth tokens via temp-file-and-rename
|
|
219
|
+
// (spike 1.2), which a symlink survives because it names a path, not an
|
|
220
|
+
// inode. Host-path only — this wizard only ever scaffolds `runtime:
|
|
221
|
+
// local`, so the operator's real ~/.pi/agent/auth.json is the same file
|
|
222
|
+
// the local agent process resolves against.
|
|
223
|
+
const authSource = join(opts.home ?? homedir(), PI_AUTH_FILE)
|
|
224
|
+
const linkPath = join(dir, `agents/zooid-assistant/${PI_AGENT_DIR}/auth.json`)
|
|
225
|
+
if (!existsSync(linkPath)) {
|
|
226
|
+
mkdirSync(dirname(linkPath), { recursive: true })
|
|
227
|
+
symlinkSync(authSource, linkPath)
|
|
228
|
+
}
|
|
229
|
+
console.log(
|
|
230
|
+
`✓ Shared pi login from ${s.path} — the agent uses your subscription via a symlink at ${PI_AGENT_DIR}/auth.json`,
|
|
231
|
+
)
|
|
232
|
+
} else if (s.found) {
|
|
233
|
+
console.log(
|
|
234
|
+
`✓ Found pi login at ${s.path} — the agent gets its own config at ${PI_AGENT_DIR}/ and will not touch it`,
|
|
235
|
+
)
|
|
236
|
+
} else {
|
|
237
|
+
console.warn(`⚠ No pi login detected — the agent uses its own API key from .env`)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
155
241
|
console.log('\nNext: zooid dev')
|
|
156
242
|
}
|