@treeport/pi 0.6.1
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/LICENSE +21 -0
- package/README.md +79 -0
- package/extensions/treeport/index.ts +231 -0
- package/extensions/treeport/treeport-cli.ts +146 -0
- package/package.json +58 -0
- package/skills/treeport/SKILL.md +219 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Noice Tech
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# `@treeport/pi`
|
|
2
|
+
|
|
3
|
+
Give Pi compact Treeport context and access to the Treeport CLI.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
Install these applications first:
|
|
8
|
+
|
|
9
|
+
- Treeport `0.5.0` or later;
|
|
10
|
+
- Pi `0.84.3` or later;
|
|
11
|
+
- Node.js 24 or later;
|
|
12
|
+
- macOS or Linux.
|
|
13
|
+
|
|
14
|
+
Make sure that the `treeport` command is on `PATH`.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
Install the package globally in Pi:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
pi install npm:@treeport/pi
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The package contains a Pi extension and the detailed Treeport skill.
|
|
25
|
+
|
|
26
|
+
The extension starts only in a terminal that Treeport manages.
|
|
27
|
+
|
|
28
|
+
A managed Pi session shows this footer status:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
treeport · <tree-name>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Outside Treeport, the extension adds no guidance, notification, or footer status.
|
|
35
|
+
|
|
36
|
+
## Use natural requests
|
|
37
|
+
|
|
38
|
+
Ask Pi to do the work. You do not have to name Treeport.
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
|
|
42
|
+
- “Start the development server.”
|
|
43
|
+
- “Stop the development server.”
|
|
44
|
+
- “Open the app and check the settings page.”
|
|
45
|
+
- “Do this side quest in a separate tree.”
|
|
46
|
+
|
|
47
|
+
In a managed session, the extension briefly defines Treeport projects and trees.
|
|
48
|
+
|
|
49
|
+
It includes the current project and tree names. It does not include IDs, paths, or the daemon URL.
|
|
50
|
+
|
|
51
|
+
The guidance tells Pi to use the `treeport` CLI through its standard Bash tool.
|
|
52
|
+
|
|
53
|
+
Pi uses a persistent terminal for a long-running process. Pi uses Bash directly for a finite command.
|
|
54
|
+
|
|
55
|
+
When you ask Pi to stop a process, Pi can delete its persistent terminal. Pi must not delete its own terminal.
|
|
56
|
+
|
|
57
|
+
For a side quest, Pi can use another terminal in the current tree. It can use `treeport spawn` for another tree.
|
|
58
|
+
|
|
59
|
+
Pi can control visible browser tabs when the Treeport CLI and daemon support browser commands.
|
|
60
|
+
|
|
61
|
+
The browser tabs stay open so you can inspect them.
|
|
62
|
+
|
|
63
|
+
The guidance requires your approval before a Chromium installation.
|
|
64
|
+
|
|
65
|
+
## Use the skill for detailed workflows
|
|
66
|
+
|
|
67
|
+
The bundled skill contains detailed Treeport CLI procedures.
|
|
68
|
+
|
|
69
|
+
Pi does not need the skill for routine terminal or browser operations.
|
|
70
|
+
|
|
71
|
+
Use the skill for lifecycle operations, remote access, updates, recovery, or complex child-tree work.
|
|
72
|
+
|
|
73
|
+
## Remove
|
|
74
|
+
|
|
75
|
+
Remove the package from Pi:
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
pi remove npm:@treeport/pi
|
|
79
|
+
```
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
|
|
2
|
+
import { Type, type Static } from 'typebox'
|
|
3
|
+
import { runTreeportJson } from './treeport-cli.ts'
|
|
4
|
+
|
|
5
|
+
const CONTEXT_TIMEOUT_MS = 5_000
|
|
6
|
+
|
|
7
|
+
interface ManagedContext {
|
|
8
|
+
project: {
|
|
9
|
+
id: string
|
|
10
|
+
name: string
|
|
11
|
+
}
|
|
12
|
+
worktree: {
|
|
13
|
+
id: string
|
|
14
|
+
projectId: string
|
|
15
|
+
name: string
|
|
16
|
+
}
|
|
17
|
+
terminal: {
|
|
18
|
+
id: string
|
|
19
|
+
worktreeId: string
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const BrowserStatusSchema = Type.Object(
|
|
24
|
+
{
|
|
25
|
+
installed: Type.Boolean(),
|
|
26
|
+
launchReady: Type.Boolean()
|
|
27
|
+
},
|
|
28
|
+
{ additionalProperties: true }
|
|
29
|
+
)
|
|
30
|
+
const ContextSchema = Type.Union([
|
|
31
|
+
Type.Object(
|
|
32
|
+
{
|
|
33
|
+
managed: Type.Literal(false),
|
|
34
|
+
reason: Type.String()
|
|
35
|
+
},
|
|
36
|
+
{ additionalProperties: true }
|
|
37
|
+
),
|
|
38
|
+
Type.Object(
|
|
39
|
+
{
|
|
40
|
+
managed: Type.Literal(true),
|
|
41
|
+
apiUrl: Type.String({ minLength: 1 }),
|
|
42
|
+
daemonLifecycle: Type.Union([
|
|
43
|
+
Type.Literal('treeport'),
|
|
44
|
+
Type.Literal('service'),
|
|
45
|
+
Type.Literal('external')
|
|
46
|
+
]),
|
|
47
|
+
project: Type.Object(
|
|
48
|
+
{
|
|
49
|
+
id: Type.String({ minLength: 1 }),
|
|
50
|
+
name: Type.String({ minLength: 1 }),
|
|
51
|
+
kind: Type.Union([Type.Literal('repository'), Type.Literal('folder')])
|
|
52
|
+
},
|
|
53
|
+
{ additionalProperties: true }
|
|
54
|
+
),
|
|
55
|
+
worktree: Type.Object(
|
|
56
|
+
{
|
|
57
|
+
id: Type.String({ minLength: 1 }),
|
|
58
|
+
projectId: Type.String({ minLength: 1 }),
|
|
59
|
+
name: Type.String({ minLength: 1 }),
|
|
60
|
+
path: Type.String({ minLength: 1 })
|
|
61
|
+
},
|
|
62
|
+
{ additionalProperties: true }
|
|
63
|
+
),
|
|
64
|
+
terminal: Type.Object(
|
|
65
|
+
{
|
|
66
|
+
id: Type.String({ minLength: 1 }),
|
|
67
|
+
worktreeId: Type.String({ minLength: 1 }),
|
|
68
|
+
name: Type.String({ minLength: 1 })
|
|
69
|
+
},
|
|
70
|
+
{ additionalProperties: true }
|
|
71
|
+
)
|
|
72
|
+
},
|
|
73
|
+
{ additionalProperties: true }
|
|
74
|
+
)
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
type ContextOutput = Static<typeof ContextSchema>
|
|
78
|
+
|
|
79
|
+
function managedContext(value: ContextOutput): ManagedContext | null {
|
|
80
|
+
if (value.managed === false) {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
value.worktree.projectId !== value.project.id ||
|
|
86
|
+
value.terminal.worktreeId !== value.worktree.id
|
|
87
|
+
) {
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let parsedApiUrl: URL
|
|
92
|
+
try {
|
|
93
|
+
parsedApiUrl = new URL(value.apiUrl)
|
|
94
|
+
} catch {
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
if (!['http:', 'https:'].includes(parsedApiUrl.protocol)) {
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
project: value.project,
|
|
103
|
+
worktree: value.worktree,
|
|
104
|
+
terminal: value.terminal
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default function treeportExtension(pi: ExtensionAPI): void {
|
|
109
|
+
let guidance: string | null = null
|
|
110
|
+
let badgeVisible = false
|
|
111
|
+
|
|
112
|
+
pi.on('session_start', async (_event, sessionContext) => {
|
|
113
|
+
guidance = null
|
|
114
|
+
let detectedValue: ContextOutput
|
|
115
|
+
try {
|
|
116
|
+
detectedValue = await runTreeportJson(pi, ['context'], ContextSchema, {
|
|
117
|
+
cwd: sessionContext.cwd,
|
|
118
|
+
signal: undefined,
|
|
119
|
+
timeout: CONTEXT_TIMEOUT_MS
|
|
120
|
+
})
|
|
121
|
+
} catch {
|
|
122
|
+
const injectedIds = [
|
|
123
|
+
process.env.TREEPORT_PROJECT_ID,
|
|
124
|
+
process.env.TREEPORT_WORKTREE_ID,
|
|
125
|
+
process.env.TREEPORT_TERMINAL_ID
|
|
126
|
+
].some((value) => Boolean(value?.trim()))
|
|
127
|
+
if (injectedIds && sessionContext.hasUI) {
|
|
128
|
+
sessionContext.ui.notify(
|
|
129
|
+
'Treeport context is unavailable. The Treeport integration is inactive.',
|
|
130
|
+
'warning'
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (detectedValue.managed === false) {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const detected = managedContext(detectedValue)
|
|
142
|
+
if (!detected) {
|
|
143
|
+
const injectedIds = [
|
|
144
|
+
process.env.TREEPORT_PROJECT_ID,
|
|
145
|
+
process.env.TREEPORT_WORKTREE_ID,
|
|
146
|
+
process.env.TREEPORT_TERMINAL_ID
|
|
147
|
+
].some((value) => Boolean(value?.trim()))
|
|
148
|
+
if (injectedIds && sessionContext.hasUI) {
|
|
149
|
+
sessionContext.ui.notify(
|
|
150
|
+
'Treeport context is invalid. The Treeport integration is inactive.',
|
|
151
|
+
'warning'
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let browserCommandsAvailable = true
|
|
159
|
+
try {
|
|
160
|
+
await runTreeportJson(pi, ['browser', 'status'], BrowserStatusSchema, {
|
|
161
|
+
cwd: sessionContext.cwd,
|
|
162
|
+
signal: undefined,
|
|
163
|
+
timeout: CONTEXT_TIMEOUT_MS
|
|
164
|
+
})
|
|
165
|
+
} catch {
|
|
166
|
+
browserCommandsAvailable = false
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const guidanceLines = [
|
|
170
|
+
'Treeport context:',
|
|
171
|
+
browserCommandsAvailable
|
|
172
|
+
? 'Treeport is a worktree-first workspace for projects, trees, persistent terminals, and browser tabs.'
|
|
173
|
+
: 'Treeport is a worktree-first workspace for projects, trees, and persistent terminals.',
|
|
174
|
+
'A project is a registered repository or folder. A tree is its main checkout or a linked Git worktree.',
|
|
175
|
+
`This session runs in project ${JSON.stringify(
|
|
176
|
+
detected.project.name
|
|
177
|
+
)} and tree ${JSON.stringify(detected.worktree.name)}.`,
|
|
178
|
+
'Use the `treeport` CLI through bash for Treeport operations. Use `--json` when you must parse a result.',
|
|
179
|
+
'Use bash directly for finite commands that Pi must await.',
|
|
180
|
+
'For a persistent process, run `treeport terminal create --worktree . --name <name> -- <program> <arg> ...`.',
|
|
181
|
+
'Pass the child program and its arguments after `--`. Do not use an implicit shell command string.',
|
|
182
|
+
'Observe persistent terminals with `treeport terminal inspect`, `treeport terminal capture`, or `treeport terminal wait`.',
|
|
183
|
+
'Do not poll through repeated model calls. Sleep and capture in one bash call, such as `sleep 5; treeport terminal capture <id>`.',
|
|
184
|
+
'`treeport terminal wait --until idle` observes OSC progress. It is not a readiness check and can return immediately.',
|
|
185
|
+
'Delete a terminal only when the user asks to stop or close its process. Never delete this Pi session terminal.',
|
|
186
|
+
'A side quest is independent work in another persistent terminal. Use `treeport terminal create` here or `treeport spawn` for another tree.',
|
|
187
|
+
...(browserCommandsAvailable
|
|
188
|
+
? [
|
|
189
|
+
'Use `treeport browser` commands for visible browser tabs. Take a new snapshot after navigation or a runtime change.',
|
|
190
|
+
'Leave browser tabs open for user inspection. Do not install Chromium without user approval.',
|
|
191
|
+
'Do not put secrets in browser URLs or command arguments.'
|
|
192
|
+
]
|
|
193
|
+
: []),
|
|
194
|
+
'Use `treeport <area> <command> --help` for exact syntax. Do not load the Treeport skill for these routine operations.'
|
|
195
|
+
]
|
|
196
|
+
guidance = guidanceLines.join('\n')
|
|
197
|
+
|
|
198
|
+
if (!browserCommandsAvailable && sessionContext.hasUI) {
|
|
199
|
+
sessionContext.ui.notify(
|
|
200
|
+
'Treeport browser commands are unavailable in this session.',
|
|
201
|
+
'warning'
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (sessionContext.hasUI) {
|
|
206
|
+
sessionContext.ui.setStatus(
|
|
207
|
+
'treeport',
|
|
208
|
+
sessionContext.ui.theme.fg(
|
|
209
|
+
'accent',
|
|
210
|
+
`treeport · ${detected.worktree.name}`
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
badgeVisible = true
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
pi.on('before_agent_start', (event) => {
|
|
218
|
+
if (!guidance) {
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${guidance}` }
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
pi.on('session_shutdown', (_event, sessionContext) => {
|
|
226
|
+
if (badgeVisible) {
|
|
227
|
+
sessionContext.ui.setStatus('treeport', undefined)
|
|
228
|
+
badgeVisible = false
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { constants as fsConstants } from 'node:fs'
|
|
2
|
+
import { access } from 'node:fs/promises'
|
|
3
|
+
import { basename, dirname, join } from 'node:path'
|
|
4
|
+
import type { ExecOptions, ExtensionAPI } from '@earendil-works/pi-coding-agent'
|
|
5
|
+
import { Type, type Static, type TSchema } from 'typebox'
|
|
6
|
+
import { Value } from 'typebox/value'
|
|
7
|
+
|
|
8
|
+
const TreeportCliErrorBodySchema = Type.Object(
|
|
9
|
+
{
|
|
10
|
+
error: Type.Object(
|
|
11
|
+
{
|
|
12
|
+
code: Type.String(),
|
|
13
|
+
message: Type.String()
|
|
14
|
+
},
|
|
15
|
+
{ additionalProperties: true }
|
|
16
|
+
)
|
|
17
|
+
},
|
|
18
|
+
{ additionalProperties: true }
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
type TreeportCliErrorBody = Static<typeof TreeportCliErrorBodySchema>
|
|
22
|
+
|
|
23
|
+
class TreeportCliError extends Error {
|
|
24
|
+
constructor(
|
|
25
|
+
readonly code: string,
|
|
26
|
+
message: string
|
|
27
|
+
) {
|
|
28
|
+
super(`[${code}] ${message}`)
|
|
29
|
+
this.name = 'TreeportCliError'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseJson<T extends TSchema>(
|
|
34
|
+
value: string,
|
|
35
|
+
schema: T
|
|
36
|
+
): Static<T> | null {
|
|
37
|
+
try {
|
|
38
|
+
return Value.Parse(schema, JSON.parse(value))
|
|
39
|
+
} catch {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function conciseError(value: string): string {
|
|
45
|
+
return (value.split(/\n\n(?:AI agents:|Usage:)/, 1)[0] ?? value).trim()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function treeportExecutable(): Promise<string> {
|
|
49
|
+
const configured = process.env.TREEPORT_CLI_ENTRYPOINT?.trim()
|
|
50
|
+
if (
|
|
51
|
+
configured &&
|
|
52
|
+
(await access(configured, fsConstants.X_OK).then(
|
|
53
|
+
() => true,
|
|
54
|
+
() => false
|
|
55
|
+
))
|
|
56
|
+
) {
|
|
57
|
+
return configured
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const daemonRecord = process.env.TREEPORT_DAEMON_RECORD?.trim()
|
|
61
|
+
if (daemonRecord) {
|
|
62
|
+
const developmentRuntime = dirname(dirname(daemonRecord))
|
|
63
|
+
if (basename(developmentRuntime) === '.treeport-dev') {
|
|
64
|
+
const developmentCli = join(
|
|
65
|
+
dirname(developmentRuntime),
|
|
66
|
+
'.treeport-dev-dist/node/cli/index.js'
|
|
67
|
+
)
|
|
68
|
+
if (
|
|
69
|
+
await access(developmentCli, fsConstants.X_OK).then(
|
|
70
|
+
() => true,
|
|
71
|
+
() => false
|
|
72
|
+
)
|
|
73
|
+
) {
|
|
74
|
+
return developmentCli
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return 'treeport'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function runTreeportJson<T extends TSchema>(
|
|
83
|
+
pi: Pick<ExtensionAPI, 'exec'>,
|
|
84
|
+
args: readonly string[],
|
|
85
|
+
schema: T,
|
|
86
|
+
options: {
|
|
87
|
+
cwd: string
|
|
88
|
+
signal: AbortSignal | undefined
|
|
89
|
+
timeout: number
|
|
90
|
+
}
|
|
91
|
+
): Promise<Static<T>> {
|
|
92
|
+
const jsonArgs = [...args]
|
|
93
|
+
const separator = jsonArgs.indexOf('--')
|
|
94
|
+
jsonArgs.splice(separator === -1 ? jsonArgs.length : separator, 0, '--json')
|
|
95
|
+
|
|
96
|
+
const execOptions: ExecOptions = {
|
|
97
|
+
cwd: options.cwd,
|
|
98
|
+
timeout: options.timeout
|
|
99
|
+
}
|
|
100
|
+
if (options.signal) {
|
|
101
|
+
execOptions.signal = options.signal
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const result = await pi.exec(
|
|
105
|
+
await treeportExecutable(),
|
|
106
|
+
jsonArgs,
|
|
107
|
+
execOptions
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if (result.code !== 0) {
|
|
111
|
+
const parsed: TreeportCliErrorBody | null = parseJson(
|
|
112
|
+
result.stderr.trim(),
|
|
113
|
+
TreeportCliErrorBodySchema
|
|
114
|
+
)
|
|
115
|
+
const code = parsed
|
|
116
|
+
? parsed.error.code
|
|
117
|
+
: result.killed
|
|
118
|
+
? options.signal?.aborted
|
|
119
|
+
? 'TREEPORT_CANCELLED'
|
|
120
|
+
: 'TREEPORT_EXECUTION_TIMEOUT'
|
|
121
|
+
: 'TREEPORT_EXECUTION_FAILED'
|
|
122
|
+
const message = parsed
|
|
123
|
+
? conciseError(parsed.error.message)
|
|
124
|
+
: result.killed
|
|
125
|
+
? options.signal?.aborted
|
|
126
|
+
? 'The Treeport command was cancelled.'
|
|
127
|
+
: 'The Treeport command reached its execution time limit.'
|
|
128
|
+
: result.stderr.trim()
|
|
129
|
+
? `${conciseError(result.stderr)} (Treeport exit code ${result.code}).`
|
|
130
|
+
: `Treeport exited with code ${result.code}.`
|
|
131
|
+
throw new TreeportCliError(code, message)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const output = result.stdout.trim()
|
|
135
|
+
const parsed: Static<T> | null = parseJson(output, schema)
|
|
136
|
+
if (parsed === null) {
|
|
137
|
+
throw new TreeportCliError(
|
|
138
|
+
'TREEPORT_INVALID_JSON',
|
|
139
|
+
output
|
|
140
|
+
? 'Treeport returned invalid JSON.'
|
|
141
|
+
: 'Treeport returned no JSON output.'
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return parsed
|
|
146
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@treeport/pi",
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "Treeport context and CLI guidance for Pi.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"extensions/treeport/index.ts",
|
|
8
|
+
"extensions/treeport/treeport-cli.ts",
|
|
9
|
+
"skills/treeport/SKILL.md",
|
|
10
|
+
"LICENSE",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"pi-package",
|
|
15
|
+
"treeport",
|
|
16
|
+
"coding-agent"
|
|
17
|
+
],
|
|
18
|
+
"pi": {
|
|
19
|
+
"extensions": [
|
|
20
|
+
"./extensions/treeport/index.ts"
|
|
21
|
+
],
|
|
22
|
+
"skills": [
|
|
23
|
+
"./skills/treeport"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=24"
|
|
28
|
+
},
|
|
29
|
+
"os": [
|
|
30
|
+
"darwin",
|
|
31
|
+
"linux"
|
|
32
|
+
],
|
|
33
|
+
"license": "Apache-2.0",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/noice-tech/treeport.git",
|
|
37
|
+
"directory": "packages/pi"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/noice-tech/treeport/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://treeport.app",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
48
|
+
"typebox": "*"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@earendil-works/pi-coding-agent": "^0.84.3",
|
|
52
|
+
"typebox": "^1.3.7"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"typecheck": "tsc --noEmit -p tsconfig.json --pretty",
|
|
56
|
+
"test:package": "node scripts/check-package.mjs"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: treeport
|
|
3
|
+
description: Detailed Treeport CLI guidance for child-tree orchestration, lifecycle, services, remote access, updates, and recovery. Use when compact Pi guidance or command help is not sufficient. Do not load for routine background processes or browser tabs in a managed Pi session.
|
|
4
|
+
compatibility: Requires the treeport CLI on PATH and a reachable Treeport daemon. Creation commands also require the requested child executable to be installed.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Treeport
|
|
8
|
+
|
|
9
|
+
In a managed Pi session, use the injected CLI guidance for routine terminal and browser operations. Load this skill only when you need more detail.
|
|
10
|
+
|
|
11
|
+
Treeport is a generic terminal and tree layer. Its terminals are persistent tmux sessions that appear in the Treeport UI. A user can open a created terminal, take control of the normal application TUI, and continue working in the same session.
|
|
12
|
+
|
|
13
|
+
Treeport does not define task sources, planning or approval states, agent tool policies, or provider-specific workflows. The caller owns names, commands, prompts, and higher-level orchestration.
|
|
14
|
+
|
|
15
|
+
## Operating rules
|
|
16
|
+
|
|
17
|
+
- Use normal CLI output when you are reading the result. Use `--json` only for programmatic extraction or branching.
|
|
18
|
+
- Treat command arguments after `--` as an argv array. Do not turn them into a shell command string.
|
|
19
|
+
- Do not use `eval` or an implicit `sh -lc`. Launch a shell explicitly only when the caller intentionally requests shell semantics.
|
|
20
|
+
- Do not place untrusted titles, prompts, or other external text into interpolated shell fragments. Pass each value as one argument or use a caller-managed file when the child supports file arguments.
|
|
21
|
+
- Never delete a terminal or remove a tree unless the user explicitly asks.
|
|
22
|
+
- Obey the daemon lifecycle reported by `treeport context`.
|
|
23
|
+
- When the lifecycle is `external`, never run `treeport start`, `treeport stop`, or `treeport remote enable`.
|
|
24
|
+
- In the external lifecycle, the parent process owns startup, shutdown, and remote exposure.
|
|
25
|
+
- When the lifecycle is `service`, normal `start` and `stop` delegate to the OS manager.
|
|
26
|
+
- Use bare `treeport update` to update a supported local npm installation. It preserves tmux terminals and an enabled service, and restarts only a daemon that was running.
|
|
27
|
+
- Never run bare `treeport update` for an external or selected remote daemon.
|
|
28
|
+
- Never invoke `sudo` for a normal service or update operation.
|
|
29
|
+
- Normal macOS service mode is a user/login LaunchAgent and does not need an administrator.
|
|
30
|
+
- Advanced headless mode is explicit.
|
|
31
|
+
- If an existing headless service prints an administrator command, report the command and wait for the user.
|
|
32
|
+
- Do not restrict a launched agent's normal tools or make it ephemeral unless the caller explicitly asks. The persistent interactive session is intended to remain useful when the user takes over.
|
|
33
|
+
|
|
34
|
+
## Understand the current context
|
|
35
|
+
|
|
36
|
+
Run:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
treeport context
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Inside a managed terminal, this reports the current project, tree, terminal, paths, statuses, IDs, tree context values, and daemon URL. User-supplied context can contain an issue identifier, a task description, or other information for the tree. Use these values as task input when the caller asks you to work from the tree context. It reports whether Treeport, the OS service, or an external process manages the daemon lifecycle. It resolves the injected IDs strictly. It does not guess identity from the current path.
|
|
43
|
+
|
|
44
|
+
Outside Treeport it reports that the terminal is not managed and exits successfully. `TREEPORT_API_URL` may be configured outside a managed terminal; if any context ID is present, however, all injected values are required. Partial IDs or IDs that no longer belong together fail instead of falling back to path inference.
|
|
45
|
+
|
|
46
|
+
Use the exact IDs from this command when an operation targets a different project or tree. `.` is a convenient shorthand for the current project or tree.
|
|
47
|
+
|
|
48
|
+
When the current folder is in the target project, omit `--project` from `worktree create` and `spawn`. Treeport detects the registered project from the current folder.
|
|
49
|
+
|
|
50
|
+
## Create a terminal in the current tree
|
|
51
|
+
|
|
52
|
+
Create a persistent login shell:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
treeport terminal create --worktree <tree-id> --name <terminal-name>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Launch a program directly:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
treeport terminal create --worktree <tree-id> --name <terminal-name> -- <program> <arg> ...
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The command returns after Treeport creates the tmux session. The program continues independently of the browser and of the caller that created it.
|
|
65
|
+
|
|
66
|
+
## Create a child tree and terminal
|
|
67
|
+
|
|
68
|
+
Create a linked tree and its first persistent terminal together:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
treeport spawn \
|
|
72
|
+
--worktree-name <tree-name> \
|
|
73
|
+
--name <terminal-name> \
|
|
74
|
+
-- <program> <arg> ...
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The child program and its arguments are entirely caller-owned. Treeport preserves them but does not add prompts, modes, capability restrictions, or lifecycle policy.
|
|
78
|
+
|
|
79
|
+
By default, Treeport bases the tree on the fetched remote default branch. Add `--from-current` only for the current tree's committed `HEAD`. Uncommitted changes are not copied.
|
|
80
|
+
|
|
81
|
+
Treeport serializes tree mutations per project. If a caller needs several child trees, create them one at a time. Their terminal programs can run concurrently after creation.
|
|
82
|
+
|
|
83
|
+
## Interpret creation results
|
|
84
|
+
|
|
85
|
+
A successful `terminal create` means the tmux session was created. The requested program can still exit later.
|
|
86
|
+
|
|
87
|
+
`spawn` is intentionally non-atomic after Git creates the worktree:
|
|
88
|
+
|
|
89
|
+
- A terminal ID means the persistent session was created.
|
|
90
|
+
- `terminalError` means the tree remains but its initial terminal could not be created.
|
|
91
|
+
- `setupError` means tree setup could not be prepared. A retained terminal may display that error and exit without launching the requested program.
|
|
92
|
+
- Setup tasks can also fail after the create response. Their output and failure remain visible in the retained terminal.
|
|
93
|
+
|
|
94
|
+
Report partial creation with the returned tree and terminal IDs. Do not blindly rerun `spawn`: the tree may already exist. Do not remove retained resources automatically.
|
|
95
|
+
|
|
96
|
+
Inspect terminal inventory later with:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
treeport terminal list --worktree <tree-id>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Inspect one terminal's refreshed process status and volatile runtime metadata with:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
treeport terminal inspect <terminal-id>
|
|
106
|
+
treeport terminal inspect <terminal-id> --json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Runtime metadata includes the title, current OSC `9;4` progress, last progress start and clear timestamps, and latest daemon-observed real BEL. BEL metadata also reports daemon-lifetime unread attention shared by every browser; inspection and waits never acknowledge it, while viewing the terminal acknowledges the exact observed BEL sequence. `.` resolves to the exact `TREEPORT_TERMINAL_ID` inside a managed terminal; it is not a name or path lookup.
|
|
110
|
+
|
|
111
|
+
Read recent terminal contents with:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
treeport terminal capture <terminal-id>
|
|
115
|
+
treeport terminal capture <terminal-id> --lines 500
|
|
116
|
+
treeport terminal capture <terminal-id> --json
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Capture returns up to 200 pane rows by default. Plain output is the terminal text; JSON output includes the terminal ID, capture time, line limit, and content. `.` can be used for the current managed terminal.
|
|
120
|
+
|
|
121
|
+
Wait for raw terminal conditions without polling or scraping output:
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
treeport terminal wait <terminal-id> --until working
|
|
125
|
+
treeport terminal wait <terminal-id> --until idle --timeout 30m
|
|
126
|
+
treeport terminal wait <terminal-id> --until bell
|
|
127
|
+
treeport terminal wait <terminal-id> --until exit
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- `idle` means no daemon-owned OSC progress is currently observed and can return immediately.
|
|
131
|
+
- `working` means daemon-owned OSC progress is currently present and can return immediately. Every valid active progress frame renews a five-minute inactivity lease; an explicit clear or terminal/observer shutdown clears immediately.
|
|
132
|
+
- `bell` means the next real BEL after the event subscription is established.
|
|
133
|
+
- `exit` means the retained terminal process has exited.
|
|
134
|
+
- Waits have no default timeout. Use a positive `ms`, `s`, `m`, or `h` duration when a deadline is required; Ctrl+C cancels.
|
|
135
|
+
|
|
136
|
+
Treeport does not infer agent settlement. An orchestrator can inspect first, wait for `working` if no progress cycle has been observed, and then wait for `idle`. Progress depends on the child application emitting OSC `9;4`; for Pi, `terminal.showTerminalProgress` must be enabled. Applications should clear progress or refresh active progress more frequently than the five-minute lease. A null progress value is not proof that every application supports progress reporting.
|
|
137
|
+
|
|
138
|
+
## Inspect the worktree application in a browser tab
|
|
139
|
+
|
|
140
|
+
The Browser primitive has one live runtime for each browser tab.
|
|
141
|
+
|
|
142
|
+
Electron owns the visible `<webview>` page for a local desktop connection.
|
|
143
|
+
|
|
144
|
+
Playwright controls managed Chromium for a web or remote desktop connection.
|
|
145
|
+
|
|
146
|
+
Never attach Treeport to a personal browser profile.
|
|
147
|
+
|
|
148
|
+
First, inspect the available browser tabs:
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
treeport browser list
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
If no browser tab is open, open one in the current tree:
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
treeport browser open --worktree .
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Use these commands on the current live page:
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
treeport browser snapshot
|
|
164
|
+
treeport browser click e12
|
|
165
|
+
treeport browser fill e14 "value"
|
|
166
|
+
treeport browser press Enter
|
|
167
|
+
treeport browser console
|
|
168
|
+
treeport browser network
|
|
169
|
+
treeport browser screenshot
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The commands select the only browser tab in the current tree.
|
|
173
|
+
|
|
174
|
+
When more than one browser tab exists, add `--panel <panel-id>`.
|
|
175
|
+
|
|
176
|
+
Snapshot references belong to one runtime generation.
|
|
177
|
+
|
|
178
|
+
Take a new snapshot after navigation or a runtime change.
|
|
179
|
+
|
|
180
|
+
A local desktop owner does not need managed Chromium.
|
|
181
|
+
|
|
182
|
+
If no local owner exists, check managed Chromium before daemon automation:
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
treeport browser status
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Ask the user before you run `treeport browser install`.
|
|
189
|
+
|
|
190
|
+
Leave the browser tab open so the user can inspect the result.
|
|
191
|
+
|
|
192
|
+
## Automation and integrations
|
|
193
|
+
|
|
194
|
+
Extensions and scripts should add `--json` before the `--` command separator:
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
treeport context --json
|
|
198
|
+
|
|
199
|
+
treeport terminal create \
|
|
200
|
+
--worktree <tree-id> \
|
|
201
|
+
--name <terminal-name> \
|
|
202
|
+
--json -- <program> <arg> ...
|
|
203
|
+
|
|
204
|
+
treeport spawn \
|
|
205
|
+
--worktree-name <tree-name> \
|
|
206
|
+
--name <terminal-name> \
|
|
207
|
+
--json -- <program> <arg> ...
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
JSON success output is written to stdout. JSON errors use `{ "error": { "code", "message", "details"? } }` on stderr. Relevant exit codes are:
|
|
211
|
+
|
|
212
|
+
- `0`: command completed; for `spawn`, still inspect `terminal`, `terminalError`, and `setupError`.
|
|
213
|
+
- `2`: invalid CLI usage.
|
|
214
|
+
- `3`: the daemon could not be reached or its event stream failed.
|
|
215
|
+
- `4`: a terminal wait timed out.
|
|
216
|
+
- `5`: API, domain, or invalid-context refusal.
|
|
217
|
+
- `130`: a terminal wait was interrupted with Ctrl+C.
|
|
218
|
+
|
|
219
|
+
Treeport trusts the local OS boundary for loopback access and delegates supported remote authentication to Tailscale Serve. Do not expose the daemon through a direct network listener, invent credentials, or put secrets in command arguments or URLs.
|