@markjaquith/agency 3.9.3 → 3.9.5
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
package/pi-extensions/agency.ts
CHANGED
|
@@ -7,10 +7,20 @@ type ExtensionAPI = {
|
|
|
7
7
|
args: string[],
|
|
8
8
|
options: { timeout: number },
|
|
9
9
|
) => Promise<{ code: number; stdout: string }>
|
|
10
|
-
on: (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
on(name: "session_start", handler: () => void): void
|
|
11
|
+
on(
|
|
12
|
+
name: "resources_discover",
|
|
13
|
+
handler: (event: {
|
|
14
|
+
cwd: string
|
|
15
|
+
}) => Promise<{ skillPaths: string[] } | undefined>,
|
|
16
|
+
): void
|
|
17
|
+
on(
|
|
18
|
+
name: "before_agent_start",
|
|
19
|
+
handler: (
|
|
20
|
+
event: { systemPrompt: string },
|
|
21
|
+
ctx: { cwd: string },
|
|
22
|
+
) => Promise<{ systemPrompt: string } | undefined>,
|
|
23
|
+
): void
|
|
14
24
|
}
|
|
15
25
|
|
|
16
26
|
type AgencyContext = {
|
|
@@ -80,15 +90,38 @@ const agencyContext = async (
|
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
export default function agencyExtension(pi: ExtensionAPI) {
|
|
83
|
-
const contexts = new Map<
|
|
93
|
+
const contexts = new Map<
|
|
94
|
+
string,
|
|
95
|
+
{
|
|
96
|
+
promise: Promise<AgencyContext | undefined>
|
|
97
|
+
expiresAt: number
|
|
98
|
+
retryDelay: number
|
|
99
|
+
}
|
|
100
|
+
>()
|
|
101
|
+
// Pi emits session_start for startup, reload, new, resume, and fork.
|
|
102
|
+
// Resource discovery follows it and must share the same cached lookup.
|
|
103
|
+
pi.on("session_start", () => contexts.clear())
|
|
104
|
+
|
|
84
105
|
const runtimeContext = (directory: string) => {
|
|
85
106
|
const key = resolve(directory)
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
107
|
+
const previous = contexts.get(key)
|
|
108
|
+
if (previous && Date.now() < previous.expiresAt) return previous.promise
|
|
109
|
+
|
|
110
|
+
const entry = {
|
|
111
|
+
promise: agencyContext(pi, key)
|
|
112
|
+
.catch(() => undefined)
|
|
113
|
+
.then((context) => {
|
|
114
|
+
entry.retryDelay = context
|
|
115
|
+
? 0
|
|
116
|
+
: Math.min((previous?.retryDelay || 500) * 2, 30_000)
|
|
117
|
+
entry.expiresAt = Date.now() + (context ? 60_000 : entry.retryDelay)
|
|
118
|
+
return context
|
|
119
|
+
}),
|
|
120
|
+
expiresAt: Infinity,
|
|
121
|
+
retryDelay: 0,
|
|
90
122
|
}
|
|
91
|
-
|
|
123
|
+
contexts.set(key, entry)
|
|
124
|
+
return entry.promise
|
|
92
125
|
}
|
|
93
126
|
|
|
94
127
|
pi.on("resources_discover", async (event: { cwd: string }) => {
|
|
@@ -1,25 +1,146 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import { constants } from "node:fs"
|
|
3
|
+
import {
|
|
4
|
+
copyFile,
|
|
5
|
+
lstat,
|
|
6
|
+
mkdir,
|
|
7
|
+
readFile,
|
|
8
|
+
rm,
|
|
9
|
+
writeFile,
|
|
10
|
+
} from "node:fs/promises"
|
|
2
11
|
import { homedir } from "node:os"
|
|
3
12
|
import { dirname, join } from "node:path"
|
|
4
13
|
|
|
14
|
+
const ownershipVersion = 1
|
|
15
|
+
|
|
16
|
+
type LifecycleResult = {
|
|
17
|
+
status: "installed" | "updated" | "removed" | "absent" | "skipped"
|
|
18
|
+
destination: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Ownership = {
|
|
22
|
+
version: typeof ownershipVersion
|
|
23
|
+
sha256: string
|
|
24
|
+
}
|
|
25
|
+
|
|
5
26
|
export const piExtensionPath = (home = homedir()) =>
|
|
6
27
|
join(home, ".pi", "agent", "extensions", "agency.ts")
|
|
7
28
|
|
|
29
|
+
export const piExtensionOwnershipPath = (destination = piExtensionPath()) =>
|
|
30
|
+
`${destination}.agency-managed.json`
|
|
31
|
+
|
|
32
|
+
const digest = (contents: Uint8Array) =>
|
|
33
|
+
createHash("sha256").update(contents).digest("hex")
|
|
34
|
+
|
|
35
|
+
const readOwnership = async (
|
|
36
|
+
destination: string,
|
|
37
|
+
): Promise<Ownership | undefined> => {
|
|
38
|
+
try {
|
|
39
|
+
const marker = await lstat(piExtensionOwnershipPath(destination))
|
|
40
|
+
if (!marker.isFile() || marker.isSymbolicLink()) return
|
|
41
|
+
const ownership = JSON.parse(
|
|
42
|
+
await readFile(piExtensionOwnershipPath(destination), "utf8"),
|
|
43
|
+
) as Partial<Ownership>
|
|
44
|
+
if (
|
|
45
|
+
ownership.version === ownershipVersion &&
|
|
46
|
+
typeof ownership.sha256 === "string"
|
|
47
|
+
)
|
|
48
|
+
return ownership as Ownership
|
|
49
|
+
} catch {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const writeOwnership = async (
|
|
55
|
+
destination: string,
|
|
56
|
+
sha256: string,
|
|
57
|
+
exclusive = false,
|
|
58
|
+
) => {
|
|
59
|
+
await writeFile(
|
|
60
|
+
piExtensionOwnershipPath(destination),
|
|
61
|
+
`${JSON.stringify({ version: ownershipVersion, sha256 })}\n`,
|
|
62
|
+
exclusive ? { flag: "wx" } : undefined,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
8
66
|
export const installPiExtension = async (
|
|
9
67
|
source = join(import.meta.dir, "..", "pi-extensions", "agency.ts"),
|
|
10
68
|
destination = piExtensionPath(),
|
|
11
|
-
) => {
|
|
69
|
+
): Promise<LifecycleResult> => {
|
|
12
70
|
await mkdir(dirname(destination), { recursive: true })
|
|
13
|
-
await
|
|
71
|
+
const sourceContents = await readFile(source)
|
|
72
|
+
const sourceDigest = digest(sourceContents)
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await copyFile(source, destination, constants.COPYFILE_EXCL)
|
|
76
|
+
try {
|
|
77
|
+
await writeOwnership(destination, sourceDigest, true)
|
|
78
|
+
} catch (error) {
|
|
79
|
+
await rm(destination, { force: true })
|
|
80
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
|
|
81
|
+
return { status: "skipped", destination }
|
|
82
|
+
}
|
|
83
|
+
throw error
|
|
84
|
+
}
|
|
85
|
+
return { status: "installed", destination }
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const ownership = await readOwnership(destination)
|
|
91
|
+
const destinationFile = await lstat(destination)
|
|
92
|
+
if (!destinationFile.isFile() || destinationFile.isSymbolicLink()) {
|
|
93
|
+
return { status: "skipped", destination }
|
|
94
|
+
}
|
|
95
|
+
const destinationDigest = digest(await readFile(destination))
|
|
96
|
+
if (!ownership || ownership.sha256 !== destinationDigest) {
|
|
97
|
+
return { status: "skipped", destination }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await copyFile(source, destination)
|
|
101
|
+
await writeOwnership(destination, sourceDigest)
|
|
102
|
+
return { status: "updated", destination }
|
|
14
103
|
}
|
|
15
104
|
|
|
16
|
-
export const uninstallPiExtension = async (
|
|
17
|
-
|
|
105
|
+
export const uninstallPiExtension = async (
|
|
106
|
+
destination = piExtensionPath(),
|
|
107
|
+
): Promise<LifecycleResult> => {
|
|
108
|
+
const ownership = await readOwnership(destination)
|
|
109
|
+
let destinationContents: Buffer
|
|
110
|
+
try {
|
|
111
|
+
const destinationFile = await lstat(destination)
|
|
112
|
+
if (!destinationFile.isFile() || destinationFile.isSymbolicLink()) {
|
|
113
|
+
return { status: "skipped", destination }
|
|
114
|
+
}
|
|
115
|
+
destinationContents = await readFile(destination)
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
|
|
118
|
+
await rm(piExtensionOwnershipPath(destination), { force: true })
|
|
119
|
+
return { status: "absent", destination }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!ownership || ownership.sha256 !== digest(destinationContents)) {
|
|
123
|
+
return { status: "skipped", destination }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await rm(destination)
|
|
127
|
+
await rm(piExtensionOwnershipPath(destination), { force: true })
|
|
128
|
+
return { status: "removed", destination }
|
|
18
129
|
}
|
|
19
130
|
|
|
20
131
|
if (import.meta.main) {
|
|
21
132
|
const command = process.argv[2] ?? "install"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
133
|
+
const result =
|
|
134
|
+
command === "install"
|
|
135
|
+
? await installPiExtension()
|
|
136
|
+
: command === "uninstall"
|
|
137
|
+
? await uninstallPiExtension()
|
|
138
|
+
: undefined
|
|
139
|
+
if (!result)
|
|
140
|
+
throw new Error(`Unknown Pi extension lifecycle command: ${command}`)
|
|
141
|
+
if (result.status === "skipped") {
|
|
142
|
+
console.warn(
|
|
143
|
+
`Agency left the existing Pi extension unchanged because it cannot prove ownership: ${result.destination}`,
|
|
144
|
+
)
|
|
145
|
+
}
|
|
25
146
|
}
|
|
@@ -116,8 +116,8 @@ const unquote = (value: string) => {
|
|
|
116
116
|
return trimmed
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
const skillInfo = (
|
|
120
|
-
const raw = readFileSync(
|
|
119
|
+
const skillInfo = (path: string) => {
|
|
120
|
+
const raw = readFileSync(path, "utf8")
|
|
121
121
|
const match = raw.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?/)
|
|
122
122
|
const frontmatter = match?.[1] ?? ""
|
|
123
123
|
const scalar = (name: string) => {
|
|
@@ -126,15 +126,15 @@ const skillInfo = (location: string) => {
|
|
|
126
126
|
))?.[1]
|
|
127
127
|
return value ? unquote(value) : undefined
|
|
128
128
|
}
|
|
129
|
-
const id = basename(
|
|
130
|
-
? basename(dirname(
|
|
131
|
-
: basename(
|
|
129
|
+
const id = basename(path) === "SKILL.md"
|
|
130
|
+
? basename(dirname(path))
|
|
131
|
+
: basename(path, extname(path))
|
|
132
132
|
const description = scalar("description")
|
|
133
133
|
return {
|
|
134
134
|
id,
|
|
135
135
|
name: scalar("name") ?? id,
|
|
136
136
|
...(description === undefined ? {} : { description }),
|
|
137
|
-
|
|
137
|
+
path,
|
|
138
138
|
content: match ? raw.slice(match[0].length) : raw,
|
|
139
139
|
}
|
|
140
140
|
}
|
|
@@ -283,8 +283,8 @@ const setup = async (context: V2PluginContext) => {
|
|
|
283
283
|
)
|
|
284
284
|
await context.skill.transform((skills) => {
|
|
285
285
|
for (const path of paths) {
|
|
286
|
-
for (const
|
|
287
|
-
skills.add(skillInfo(
|
|
286
|
+
for (const skillPath of skillFiles(path)) {
|
|
287
|
+
skills.add(skillInfo(skillPath) as unknown as Parameters<typeof skills.add>[0])
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
290
|
})
|