pi-scout 0.1.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/CHANGELOG.md +22 -0
- package/CODE_OF_CONDUCT.md +41 -0
- package/CONTRIBUTING.md +50 -0
- package/LICENSE +21 -0
- package/README.md +89 -0
- package/SECURITY.md +26 -0
- package/extensions/index.ts +147 -0
- package/index.ts +1 -0
- package/package.json +65 -0
- package/src/index.ts +5 -0
- package/src/prompt.ts +9 -0
- package/src/repo.ts +111 -0
- package/src/state.ts +83 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
This project follows the spirit of [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and uses semantic versioning for releases.
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - TBD
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Initial `pi-scout` package scaffold.
|
|
14
|
+
- `/scout` command for registering, listing, and removing reference repositories.
|
|
15
|
+
- `scout_add` and conditional `scout_rm` tools.
|
|
16
|
+
- Compact system prompt injection for registered local repository paths with stale-temp pruning.
|
|
17
|
+
- GitHub `owner/repo` shorthand for repository registration.
|
|
18
|
+
- `scout_add` tool schema minimized to a single `source` parameter.
|
|
19
|
+
- Repository registration now defaults to shallow clones with depth `1`.
|
|
20
|
+
- Unix-like systems now use `/tmp/pi-scout` for shorter registered paths.
|
|
21
|
+
- Package-level extension entry point so Pi displays the extension as `pi-scout` instead of `extensions`/`extension`.
|
|
22
|
+
- Optional temporary clone deletion when removing repository records.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
|
|
6
|
+
|
|
7
|
+
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
|
8
|
+
|
|
9
|
+
## Our Standards
|
|
10
|
+
|
|
11
|
+
Examples of behavior that contributes to a positive environment include:
|
|
12
|
+
|
|
13
|
+
- Demonstrating empathy and kindness toward other people
|
|
14
|
+
- Being respectful of differing opinions, viewpoints, and experiences
|
|
15
|
+
- Giving and gracefully accepting constructive feedback
|
|
16
|
+
- Accepting responsibility and apologizing to those affected by our mistakes
|
|
17
|
+
- Focusing on what is best not just for us as individuals, but for the overall community
|
|
18
|
+
|
|
19
|
+
Examples of unacceptable behavior include:
|
|
20
|
+
|
|
21
|
+
- The use of sexualized language or imagery, and sexual attention or advances
|
|
22
|
+
- Trolling, insulting or derogatory comments, and personal or political attacks
|
|
23
|
+
- Public or private harassment
|
|
24
|
+
- Publishing others' private information without explicit permission
|
|
25
|
+
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
|
26
|
+
|
|
27
|
+
## Enforcement Responsibilities
|
|
28
|
+
|
|
29
|
+
Project maintainers are responsible for clarifying and enforcing standards of acceptable behavior and may remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct.
|
|
30
|
+
|
|
31
|
+
## Scope
|
|
32
|
+
|
|
33
|
+
This Code of Conduct applies within all project spaces and also applies when an individual is officially representing the project in public spaces.
|
|
34
|
+
|
|
35
|
+
## Enforcement
|
|
36
|
+
|
|
37
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project maintainers through GitHub. All complaints will be reviewed and investigated promptly and fairly.
|
|
38
|
+
|
|
39
|
+
## Attribution
|
|
40
|
+
|
|
41
|
+
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in contributing to `pi-scout`.
|
|
4
|
+
|
|
5
|
+
## Development setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run check
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
This package is source-distributed: Pi loads the TypeScript extension files directly. There is no build step for runtime use.
|
|
13
|
+
|
|
14
|
+
## Local testing
|
|
15
|
+
|
|
16
|
+
Install this checkout into a temporary Pi project:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
mkdir -p <test-project>
|
|
20
|
+
cd <test-project>
|
|
21
|
+
pi install -l /path/to/pi-mono/packages/pi-scout
|
|
22
|
+
pi
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For a one-off run without changing settings:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pi -e /path/to/pi-mono/packages/pi-scout --print "list your tools"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Pull request checklist
|
|
32
|
+
|
|
33
|
+
Before opening a pull request:
|
|
34
|
+
|
|
35
|
+
- Run `npm run check`.
|
|
36
|
+
- Run `npm audit --omit=dev`.
|
|
37
|
+
- Run `npm run pack:dry-run` and confirm the package contents are intentional.
|
|
38
|
+
- Update `README.md` if user-facing behavior changes.
|
|
39
|
+
- Update `CHANGELOG.md` for notable changes.
|
|
40
|
+
- Keep examples and paths generic; do not commit machine-specific paths, API keys, tokens, or provider config containing secrets.
|
|
41
|
+
|
|
42
|
+
## Coding guidelines
|
|
43
|
+
|
|
44
|
+
- Keep `extensions/index.ts` focused on Pi extension wiring and move reusable implementation details into `src/`.
|
|
45
|
+
- When adding tools, update Typebox schemas, runtime validation, README parameter docs, and tests together.
|
|
46
|
+
- Treat config keys, environment variables, and command names as public interface; changes to defaults or precedence are breaking changes.
|
|
47
|
+
|
|
48
|
+
## Code of conduct
|
|
49
|
+
|
|
50
|
+
This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jose Mocito
|
|
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,89 @@
|
|
|
1
|
+
# pi-scout
|
|
2
|
+
|
|
3
|
+
A source-distributed [Pi](https://pi.dev) package for registering local reference codebases for agent exploration.
|
|
4
|
+
|
|
5
|
+
> [!WARNING]
|
|
6
|
+
> Pi packages can execute arbitrary code through extensions. Review package source before installing any third-party Pi package.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- `/scout` slash command with a simple TUI flow for registering, listing, and removing reference repositories.
|
|
11
|
+
- `scout_add` tool for cloning a Git repository into a local temporary cache.
|
|
12
|
+
- `scout_rm` tool for agent-driven removal once repositories exist.
|
|
13
|
+
- Compact per-turn system prompt guidance with registered repo names and local clone paths.
|
|
14
|
+
- Automatic pruning: if the OS cleans a temporary clone, Pi Scout removes that stale entry before adding prompt context.
|
|
15
|
+
|
|
16
|
+
Registered repositories are cloned under `/tmp/pi-scout/<name>-<id>` on Unix-like systems, or the OS temp directory on Windows. Set `PI_SCOUT_TMPDIR` to override the parent temp directory. Pi Scout uses shallow clones with depth `1` by default because it is for code exploration, not history exploration. Pi Scout keeps records in Pi's agent directory and reuses them across sessions while the cloned directories still exist.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Install from npm:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pi install npm:pi-scout
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Install project-locally with Pi's `-l` flag:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pi install -l npm:pi-scout
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
During local development from this monorepo:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pi install /path/to/pi-mono/packages/pi-scout
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For a one-off test run without installing:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pi -e /path/to/pi-mono/packages/pi-scout --print "list your tools"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This is an npm-compatible TypeScript Pi package. There is no runtime build step.
|
|
45
|
+
|
|
46
|
+
## Quick usage
|
|
47
|
+
|
|
48
|
+
Open the Pi Scout menu:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
/scout
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Register a repository directly with a Git URL/path or GitHub shorthand:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
/scout https://github.com/owner/repo.git
|
|
58
|
+
/scout owner/repo
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Ask the agent to register one:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
Register https://github.com/owner/repo.git with Pi Scout, then inspect how it implements feature flags.
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
After a repository is registered, the agent sees its local path in the system prompt and can inspect it with local file tools.
|
|
68
|
+
|
|
69
|
+
## Tools
|
|
70
|
+
|
|
71
|
+
| Tool | Purpose |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `scout_add` | Clone and register a Git repository as a local reference codebase. Takes only `source`: Git URL, local path, or GitHub `owner/repo` shorthand. |
|
|
74
|
+
| `scout_rm` | Remove a repository from Pi Scout records, optionally deleting the temporary clone. Available to the model only while repos are registered. |
|
|
75
|
+
|
|
76
|
+
## Notes
|
|
77
|
+
|
|
78
|
+
- Pi Scout uses local file access for exploration. It does not provide web search or remote content-fetching tools.
|
|
79
|
+
- Registering a Git URL still uses `git clone`, so Git may contact the configured remote.
|
|
80
|
+
- Registered repositories are intended as read-only references unless the user explicitly asks otherwise.
|
|
81
|
+
- The system prompt includes only registered repo names and local paths, not origin URLs or branch metadata.
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm install
|
|
87
|
+
npm run check
|
|
88
|
+
npm run pack:dry-run
|
|
89
|
+
```
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
Security fixes are provided for the latest released version of `pi-scout`.
|
|
6
|
+
|
|
7
|
+
## Reporting a vulnerability
|
|
8
|
+
|
|
9
|
+
Please do not open a public issue for suspected security vulnerabilities.
|
|
10
|
+
|
|
11
|
+
Report privately by contacting the repository maintainer through GitHub. Include:
|
|
12
|
+
|
|
13
|
+
- a description of the issue;
|
|
14
|
+
- steps to reproduce;
|
|
15
|
+
- affected versions or commits, if known;
|
|
16
|
+
- any suggested mitigation.
|
|
17
|
+
|
|
18
|
+
The maintainer will acknowledge reports as soon as practical and coordinate disclosure once a fix or mitigation is available.
|
|
19
|
+
|
|
20
|
+
## Security model
|
|
21
|
+
|
|
22
|
+
`pi-scout` is a Pi package. Pi extensions execute with the same permissions as the local user running Pi. Users should review installed Pi packages and only install packages from sources they trust.
|
|
23
|
+
|
|
24
|
+
Do not commit API keys, tokens, credentials, local settings, or machine-specific paths.
|
|
25
|
+
|
|
26
|
+
`pi-scout` stores repository records under Pi's agent directory and clones registered repositories into the OS temporary directory. It does not provide web search or content-fetching tools, but registering a Git URL uses `git clone`, which may contact the configured remote. Registered repository paths are appended to the system prompt so the agent can inspect them with local file tools.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { buildScoutPrompt, formatRepo, loadPrunedState, registerRepo, removeRepo } from "../src/index.js";
|
|
4
|
+
|
|
5
|
+
const RegisterRepoParams = Type.Object({
|
|
6
|
+
source: Type.String({ description: "Git URL/path or owner/repo." }),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const RemoveRepoParams = Type.Object({
|
|
10
|
+
idOrName: Type.String({ description: "Repo id or name." }),
|
|
11
|
+
deleteClone: Type.Optional(Type.Boolean({ description: "Delete temp clone too." })),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export default function piScout(pi: ExtensionAPI) {
|
|
15
|
+
let scoutRmRegistered = false;
|
|
16
|
+
|
|
17
|
+
function setToolActive(name: string, active: boolean): void {
|
|
18
|
+
const activeTools = pi.getActiveTools();
|
|
19
|
+
const hasTool = activeTools.includes(name);
|
|
20
|
+
if (active && !hasTool) pi.setActiveTools([...activeTools, name]);
|
|
21
|
+
if (!active && hasTool) pi.setActiveTools(activeTools.filter((tool) => tool !== name));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function syncScoutRmTool(): Promise<void> {
|
|
25
|
+
const hasRepos = (await loadPrunedState()).repos.length > 0;
|
|
26
|
+
if (hasRepos && !scoutRmRegistered) {
|
|
27
|
+
pi.registerTool({
|
|
28
|
+
name: "scout_rm",
|
|
29
|
+
label: "Scout Remove",
|
|
30
|
+
description: "Remove a Scout repo record.",
|
|
31
|
+
promptSnippet: "Remove Scout repo records.",
|
|
32
|
+
promptGuidelines: [
|
|
33
|
+
"Use scout_rm only when asked to unregister a Scout repo.",
|
|
34
|
+
],
|
|
35
|
+
parameters: RemoveRepoParams,
|
|
36
|
+
async execute(_toolCallId, rawParams) {
|
|
37
|
+
const params = rawParams as { idOrName: string; deleteClone?: boolean };
|
|
38
|
+
const removed = await removeRepo(params.idOrName, { deleteClone: params.deleteClone });
|
|
39
|
+
await syncScoutRmTool();
|
|
40
|
+
const text = removed
|
|
41
|
+
? `Removed Pi Scout repository from records:\n${formatRepo(removed)}\n\nLocal clone ${params.deleteClone ? "deleted" : "was not deleted"}.`
|
|
42
|
+
: `No Pi Scout repository matched "${params.idOrName}".`;
|
|
43
|
+
return { content: [{ type: "text", text }], details: { removed, deletedClone: Boolean(params.deleteClone && removed) } };
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
scoutRmRegistered = true;
|
|
47
|
+
}
|
|
48
|
+
if (scoutRmRegistered) setToolActive("scout_rm", hasRepos);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pi.on("session_start", async () => {
|
|
52
|
+
await syncScoutRmTool();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
pi.registerCommand("scout", {
|
|
56
|
+
description: "Manage Scout reference repos",
|
|
57
|
+
handler: async (args, ctx) => {
|
|
58
|
+
await handleScoutCommand(pi, args, ctx, syncScoutRmTool);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
pi.registerTool({
|
|
63
|
+
name: "scout_add",
|
|
64
|
+
label: "Scout Add",
|
|
65
|
+
description: "Clone/register a reference repo.",
|
|
66
|
+
promptSnippet: "Add Scout reference repos.",
|
|
67
|
+
promptGuidelines: [
|
|
68
|
+
"Use scout_add to register a Git repo for code exploration.",
|
|
69
|
+
],
|
|
70
|
+
parameters: RegisterRepoParams,
|
|
71
|
+
async execute(_toolCallId, rawParams, signal) {
|
|
72
|
+
const params = rawParams as { source: string };
|
|
73
|
+
const repo = await registerRepo(pi, { source: params.source, signal });
|
|
74
|
+
await syncScoutRmTool();
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text", text: `Registered Pi Scout repository:\n${formatRepo(repo)}` }],
|
|
77
|
+
details: { repo },
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
pi.on("before_agent_start", async (event) => {
|
|
84
|
+
await syncScoutRmTool();
|
|
85
|
+
const state = await loadPrunedState();
|
|
86
|
+
const scoutPrompt = buildScoutPrompt(state.repos);
|
|
87
|
+
if (!scoutPrompt) return;
|
|
88
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${scoutPrompt}` };
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function handleScoutCommand(
|
|
93
|
+
pi: ExtensionAPI,
|
|
94
|
+
args: string,
|
|
95
|
+
ctx: ExtensionCommandContext,
|
|
96
|
+
syncScoutRmTool: () => Promise<void>,
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
const trimmed = args.trim();
|
|
99
|
+
if (trimmed) {
|
|
100
|
+
const repo = await registerRepo(pi, { source: trimmed });
|
|
101
|
+
await syncScoutRmTool();
|
|
102
|
+
ctx.ui.notify(`Pi Scout registered ${repo.name}`, "info");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const action = await ctx.ui.select("Pi Scout", [
|
|
107
|
+
"Register repository",
|
|
108
|
+
"List repositories",
|
|
109
|
+
"Remove repository",
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
if (action === "Register repository") {
|
|
113
|
+
const source = await ctx.ui.input("Repository URL or local path", "https://github.com/owner/repo.git");
|
|
114
|
+
if (!source?.trim()) return;
|
|
115
|
+
const name = await ctx.ui.input("Optional friendly name", "");
|
|
116
|
+
const repo = await registerRepo(pi, { source, name: name?.trim() || undefined });
|
|
117
|
+
await syncScoutRmTool();
|
|
118
|
+
ctx.ui.notify(`Pi Scout registered ${repo.name}`, "info");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (action === "List repositories") {
|
|
123
|
+
const state = await loadPrunedState();
|
|
124
|
+
const text = state.repos.length === 0
|
|
125
|
+
? "No Pi Scout repositories are currently registered."
|
|
126
|
+
: state.repos.map(formatRepo).join("\n\n");
|
|
127
|
+
await ctx.ui.editor("Pi Scout repositories", text);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (action === "Remove repository") {
|
|
132
|
+
const state = await loadPrunedState();
|
|
133
|
+
if (state.repos.length === 0) {
|
|
134
|
+
ctx.ui.notify("No Pi Scout repositories are currently registered.", "info");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const labels = state.repos.map((repo) => `${repo.name} (${repo.id})`);
|
|
138
|
+
const selected = await ctx.ui.select("Remove repository", labels);
|
|
139
|
+
if (!selected) return;
|
|
140
|
+
const id = selected.match(/\(([^)]+)\)$/)?.[1];
|
|
141
|
+
if (!id) return;
|
|
142
|
+
const deleteClone = await ctx.ui.confirm("Delete local clone?", "Also delete the cloned temporary directory?");
|
|
143
|
+
const removed = await removeRepo(id, { deleteClone });
|
|
144
|
+
await syncScoutRmTool();
|
|
145
|
+
if (removed) ctx.ui.notify(`Removed ${removed.name}${deleteClone ? " and deleted its clone" : " from Pi Scout records"}`, "info");
|
|
146
|
+
}
|
|
147
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./extensions/index.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-scout",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Register local reference codebases for Pi agent exploration.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "jvm",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/jvm/pi-mono.git",
|
|
11
|
+
"directory": "packages/pi-scout"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/jvm/pi-mono/issues"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/jvm/pi-mono/tree/main/packages/pi-scout#readme",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"pi-package",
|
|
19
|
+
"pi-extension",
|
|
20
|
+
"pi",
|
|
21
|
+
"scout",
|
|
22
|
+
"codebase-exploration",
|
|
23
|
+
"reference-repos"
|
|
24
|
+
],
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./src/index.ts"
|
|
27
|
+
},
|
|
28
|
+
"pi": {
|
|
29
|
+
"extensions": [
|
|
30
|
+
"./index.ts"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"index.ts",
|
|
35
|
+
"extensions",
|
|
36
|
+
"src",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"CHANGELOG.md",
|
|
40
|
+
"SECURITY.md",
|
|
41
|
+
"CONTRIBUTING.md",
|
|
42
|
+
"CODE_OF_CONDUCT.md"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"check": "tsc --noEmit",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"pack:dry-run": "npm pack --dry-run"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
51
|
+
"typebox": "*"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
55
|
+
"@types/node": "^25.6.2",
|
|
56
|
+
"typebox": "^1.1.33",
|
|
57
|
+
"typescript": "^6.0.3"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=20.6.0"
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/index.ts
ADDED
package/src/prompt.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ScoutRepo } from "./state.js";
|
|
2
|
+
|
|
3
|
+
export function buildScoutPrompt(repos: ScoutRepo[]): string {
|
|
4
|
+
if (repos.length === 0) return "";
|
|
5
|
+
|
|
6
|
+
const repoList = repos.map((repo) => `- ${repo.name}: ${repo.path}`).join("\n");
|
|
7
|
+
|
|
8
|
+
return `Scout repos:\n${repoList}\nUse as read-only reference codebases when relevant.`;
|
|
9
|
+
}
|
package/src/repo.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { platform, tmpdir } from "node:os";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { loadPrunedState, saveState, type ScoutRepo } from "./state.js";
|
|
7
|
+
|
|
8
|
+
export interface RegisterRepoOptions {
|
|
9
|
+
source: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
branch?: string;
|
|
12
|
+
depth?: number;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function registerRepo(pi: ExtensionAPI, options: RegisterRepoOptions): Promise<ScoutRepo> {
|
|
17
|
+
const source = normalizeSource(options.source.trim());
|
|
18
|
+
if (!source) throw new Error("Repository source is required.");
|
|
19
|
+
|
|
20
|
+
const id = randomUUID().slice(0, 12);
|
|
21
|
+
const name = sanitizeName(options.name?.trim() || inferName(source) || `repo-${id}`);
|
|
22
|
+
const root = getScoutCloneRoot();
|
|
23
|
+
const destination = join(root, `${name}-${id}`);
|
|
24
|
+
await mkdir(root, { recursive: true });
|
|
25
|
+
|
|
26
|
+
const args = ["clone"];
|
|
27
|
+
if (options.branch?.trim()) args.push("--branch", options.branch.trim());
|
|
28
|
+
const depth = options.depth && Number.isInteger(options.depth) && options.depth > 0 ? options.depth : 1;
|
|
29
|
+
args.push("--depth", String(depth));
|
|
30
|
+
args.push(source, destination);
|
|
31
|
+
|
|
32
|
+
const result = await pi.exec("git", args, { signal: options.signal, timeout: 120_000 });
|
|
33
|
+
if (result.code !== 0) {
|
|
34
|
+
const stderr = result.stderr?.trim() || result.stdout?.trim() || "git clone failed";
|
|
35
|
+
throw new Error(stderr);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const now = new Date().toISOString();
|
|
39
|
+
const repo: ScoutRepo = {
|
|
40
|
+
id,
|
|
41
|
+
name,
|
|
42
|
+
source,
|
|
43
|
+
path: destination,
|
|
44
|
+
branch: options.branch?.trim() || undefined,
|
|
45
|
+
createdAt: now,
|
|
46
|
+
lastSeenAt: now,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const state = await loadPrunedState();
|
|
50
|
+
state.repos.push(repo);
|
|
51
|
+
await saveState(state);
|
|
52
|
+
return repo;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function removeRepo(idOrName: string, options: { deleteClone?: boolean } = {}): Promise<ScoutRepo | undefined> {
|
|
56
|
+
const needle = idOrName.trim();
|
|
57
|
+
if (!needle) return undefined;
|
|
58
|
+
|
|
59
|
+
const state = await loadPrunedState();
|
|
60
|
+
const index = state.repos.findIndex((repo) => repo.id === needle || repo.name === needle);
|
|
61
|
+
if (index === -1) return undefined;
|
|
62
|
+
|
|
63
|
+
const [removed] = state.repos.splice(index, 1);
|
|
64
|
+
await saveState(state);
|
|
65
|
+
|
|
66
|
+
if (options.deleteClone && removed) {
|
|
67
|
+
await rm(removed.path, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return removed;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function formatRepo(repo: ScoutRepo): string {
|
|
74
|
+
const branch = repo.branch ? ` (${repo.branch})` : "";
|
|
75
|
+
return `${repo.name}${branch}\n id: ${repo.id}\n source: ${repo.source}\n path: ${repo.path}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getScoutCloneRoot(): string {
|
|
79
|
+
if (process.env.PI_SCOUT_TMPDIR) return join(process.env.PI_SCOUT_TMPDIR, "pi-scout");
|
|
80
|
+
if (platform() !== "win32") return "/tmp/pi-scout";
|
|
81
|
+
return join(tmpdir(), "pi-scout");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function inferName(source: string): string {
|
|
85
|
+
const shorthand = parseGitHubShorthand(source);
|
|
86
|
+
if (shorthand) return shorthand.repo;
|
|
87
|
+
|
|
88
|
+
const withoutTrailingSlash = source.replace(/[\\/]+$/, "");
|
|
89
|
+
const last = basename(withoutTrailingSlash).replace(/\.git$/i, "");
|
|
90
|
+
return last || "repo";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeSource(source: string): string {
|
|
94
|
+
const shorthand = parseGitHubShorthand(source);
|
|
95
|
+
if (!shorthand) return source;
|
|
96
|
+
return `https://github.com/${shorthand.owner}/${shorthand.repo}.git`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseGitHubShorthand(source: string): { owner: string; repo: string } | undefined {
|
|
100
|
+
const match = source.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/);
|
|
101
|
+
if (!match) return undefined;
|
|
102
|
+
return { owner: match[1]!, repo: match[2]!.replace(/\.git$/i, "") };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function sanitizeName(name: string): string {
|
|
106
|
+
return name
|
|
107
|
+
.toLowerCase()
|
|
108
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
109
|
+
.replace(/^-+|-+$/g, "")
|
|
110
|
+
.slice(0, 64) || "repo";
|
|
111
|
+
}
|
package/src/state.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
export interface ScoutRepo {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
source: string;
|
|
9
|
+
path: string;
|
|
10
|
+
branch?: string;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
lastSeenAt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ScoutState {
|
|
16
|
+
repos: ScoutRepo[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const STATE_DIR = join(getAgentDir(), "scout");
|
|
20
|
+
const STATE_PATH = join(STATE_DIR, "repos.json");
|
|
21
|
+
|
|
22
|
+
export function getScoutStatePath(): string {
|
|
23
|
+
return STATE_PATH;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function loadState(): Promise<ScoutState> {
|
|
27
|
+
try {
|
|
28
|
+
const raw = await readFile(STATE_PATH, "utf8");
|
|
29
|
+
const parsed = JSON.parse(raw) as Partial<ScoutState>;
|
|
30
|
+
return { repos: Array.isArray(parsed.repos) ? parsed.repos.filter(isScoutRepo) : [] };
|
|
31
|
+
} catch (error: any) {
|
|
32
|
+
if (error?.code === "ENOENT") return { repos: [] };
|
|
33
|
+
throw new Error(`Failed to read pi-scout state at ${STATE_PATH}: ${error?.message ?? String(error)}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function saveState(state: ScoutState): Promise<void> {
|
|
38
|
+
await mkdir(STATE_DIR, { recursive: true });
|
|
39
|
+
await writeFile(STATE_PATH, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function pruneMissingRepos(state: ScoutState): Promise<{ state: ScoutState; removed: ScoutRepo[] }> {
|
|
43
|
+
const repos: ScoutRepo[] = [];
|
|
44
|
+
const removed: ScoutRepo[] = [];
|
|
45
|
+
const now = new Date().toISOString();
|
|
46
|
+
|
|
47
|
+
for (const repo of state.repos) {
|
|
48
|
+
if (await pathExists(repo.path)) {
|
|
49
|
+
repos.push({ ...repo, lastSeenAt: now });
|
|
50
|
+
} else {
|
|
51
|
+
removed.push(repo);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const next = { repos };
|
|
56
|
+
if (removed.length > 0) await saveState(next);
|
|
57
|
+
return { state: next, removed };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function loadPrunedState(): Promise<ScoutState> {
|
|
61
|
+
return (await pruneMissingRepos(await loadState())).state;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
65
|
+
try {
|
|
66
|
+
await stat(path);
|
|
67
|
+
return true;
|
|
68
|
+
} catch (error: any) {
|
|
69
|
+
if (error?.code === "ENOENT") return false;
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isScoutRepo(value: unknown): value is ScoutRepo {
|
|
75
|
+
if (!value || typeof value !== "object") return false;
|
|
76
|
+
const repo = value as Record<string, unknown>;
|
|
77
|
+
return typeof repo.id === "string"
|
|
78
|
+
&& typeof repo.name === "string"
|
|
79
|
+
&& typeof repo.source === "string"
|
|
80
|
+
&& typeof repo.path === "string"
|
|
81
|
+
&& typeof repo.createdAt === "string"
|
|
82
|
+
&& typeof repo.lastSeenAt === "string";
|
|
83
|
+
}
|