portable-agent-layer 0.88.1 → 0.89.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/README.md +8 -5
- package/assets/agents/gemini-researcher.md +13 -0
- package/assets/agents/grok-researcher.md +13 -0
- package/assets/agents/multi-perspective-researcher.md +12 -0
- package/assets/agents/perplexity-researcher.md +13 -0
- package/assets/agents/skill-author.md +17 -0
- package/assets/agents/subagent-author.md +18 -1
- package/assets/skills/create-subagent/SKILL.md +3 -2
- package/assets/skills/create-subagent/authoring-guide.md +16 -0
- package/assets/templates/PAL/MEMORY_SYSTEM.md +1 -1
- package/assets/templates/PAL/README.md +6 -5
- package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +4 -1
- package/assets/templates/hooks.antigravity.json +42 -0
- package/assets/templates/settings.antigravity.json +44 -0
- package/package.json +1 -1
- package/src/cli/builtin-tools.ts +28 -23
- package/src/cli/doctor/agents.ts +64 -64
- package/src/cli/identity.ts +26 -20
- package/src/cli/index.ts +109 -312
- package/src/cli/knowledge.ts +160 -201
- package/src/cli/ledger-command.ts +69 -0
- package/src/cli/ledger.ts +20 -101
- package/src/cli/personal-context.ts +15 -12
- package/src/cli/rule.ts +29 -29
- package/src/cli/server.ts +33 -52
- package/src/cli/session-agent.ts +7 -9
- package/src/cli/skill.ts +73 -64
- package/src/cli/subagent.ts +51 -58
- package/src/cli/tree.ts +177 -0
- package/src/hooks/InvocationContext.ts +26 -0
- package/src/hooks/LedgerCommit.ts +4 -2
- package/src/hooks/LedgerSnapshot.ts +2 -0
- package/src/hooks/SecurityValidator.ts +2 -0
- package/src/hooks/StopOrchestrator.ts +12 -2
- package/src/hooks/UserPromptOrchestrator.ts +3 -15
- package/src/hooks/handlers/context-digests.ts +21 -21
- package/src/hooks/handlers/readme-sync.ts +6 -2
- package/src/hooks/lib/agent-definition.ts +6 -16
- package/src/hooks/lib/agent-registry.ts +88 -0
- package/src/hooks/lib/agent.ts +47 -23
- package/src/hooks/lib/antigravity-inference.ts +84 -0
- package/src/hooks/lib/antigravity-transcript.ts +129 -0
- package/src/hooks/lib/claim-check.ts +13 -2
- package/src/hooks/lib/claim-log.ts +5 -1
- package/src/hooks/lib/claude-md.ts +21 -1
- package/src/hooks/lib/context.ts +5 -1
- package/src/hooks/lib/export-manifest.ts +23 -0
- package/src/hooks/lib/export.ts +1 -24
- package/src/hooks/lib/hook-turn.ts +25 -2
- package/src/hooks/lib/import-merge.ts +1 -1
- package/src/hooks/lib/inference.ts +77 -1
- package/src/hooks/lib/invocation-context.ts +99 -0
- package/src/hooks/lib/ledger-hook.ts +14 -6
- package/src/hooks/lib/models.ts +34 -17
- package/src/hooks/lib/paths.ts +39 -6
- package/src/hooks/lib/prompt-capture.ts +28 -0
- package/src/hooks/lib/readme-sync.ts +3 -31
- package/src/hooks/lib/security-gate.ts +10 -3
- package/src/hooks/lib/semi-static.ts +13 -3
- package/src/hooks/lib/session-context.ts +3 -1
- package/src/hooks/lib/stop.ts +1 -0
- package/src/hooks/lib/transcript.ts +6 -3
- package/src/targets/agent-render.ts +3 -3
- package/src/targets/antigravity/install.ts +68 -0
- package/src/targets/antigravity/uninstall.ts +66 -0
- package/src/targets/lib.ts +81 -64
- package/src/targets/opencode/install.ts +2 -0
- package/src/targets/opencode/permission.ts +39 -0
- package/src/targets/opencode/uninstall.ts +9 -8
- package/src/tools/agent/algorithm-reflect.ts +70 -62
- package/src/tools/agent/algorithm-synthesize.ts +39 -14
- package/src/tools/agent/analyze.ts +31 -36
- package/src/tools/agent/handoff-note.ts +51 -54
- package/src/tools/agent/interaction.ts +32 -28
- package/src/tools/agent/project.ts +300 -287
- package/src/tools/agent/relationship-note.ts +40 -36
- package/src/tools/agent/relationship-reflect.ts +23 -32
- package/src/tools/agent/synthesize.ts +26 -27
- package/src/tools/agent/thread.ts +42 -51
- package/src/tools/agent/wisdom-frame.ts +45 -38
- package/src/tools/control-room/server.ts +0 -2
- package/src/tools/control-room/ui/dist/assets/{index-BZMDzWFa.js → index-mFpXpiBj.js} +1 -1
- package/src/tools/control-room/ui/dist/index.html +1 -1
- package/src/tools/control-room/ui/screens/log.tsx +10 -1
- package/src/tools/lib/command.ts +275 -0
- package/src/tools/subagent-doctor.ts +74 -9
- package/src/tools/token-cost.ts +18 -17
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* One entry here = the only change needed to add a new source across all consumers:
|
|
5
5
|
* CLAUDE.md @imports, opencode instructions[], Cursor .mdc, Copilot .instructions.md,
|
|
6
|
-
* and the session-stop digest writer.
|
|
6
|
+
* Antigravity plugin rules, and the session-stop digest writer.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -22,9 +22,9 @@ interface SemiStaticSource {
|
|
|
22
22
|
readonly writesDigest: boolean;
|
|
23
23
|
/** Returns current content — builds fresh when writesDigest is true, reads the file otherwise. */
|
|
24
24
|
load(): string;
|
|
25
|
-
/** Slug for
|
|
25
|
+
/** Slug for the pal-${slug} rule/instruction files written for Cursor, Copilot and Antigravity. */
|
|
26
26
|
readonly slug: string;
|
|
27
|
-
/** Human-readable description for Cursor .mdc frontmatter. */
|
|
27
|
+
/** Human-readable description for Cursor .mdc and Antigravity rule frontmatter. */
|
|
28
28
|
readonly description: string;
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -38,6 +38,16 @@ export function copilotFilename(src: SemiStaticSource): string {
|
|
|
38
38
|
return `pal-${src.slug}.instructions.md`;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/** Returns the Antigravity plugin rule filename for a source. */
|
|
42
|
+
export function antigravityFilename(src: SemiStaticSource): string {
|
|
43
|
+
return `pal-${src.slug}.md`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** An Antigravity rule file; a rule without a valid `trigger` is silently discarded. */
|
|
47
|
+
export function antigravityRule(description: string, content: string): string {
|
|
48
|
+
return `---\ntrigger: always_on\ndescription: ${description}\n---\n\n${content}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
function readFileSafe(path: string): string {
|
|
42
52
|
try {
|
|
43
53
|
if (!existsSync(path)) return "";
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* context for weeks — and none of it was reachable from a test.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import type { AgentName } from "./agent-registry";
|
|
11
|
+
|
|
10
12
|
export interface ContextEnvelope {
|
|
11
13
|
/** Whether the agent parses stdout as JSON or reads it as raw text. */
|
|
12
14
|
kind: "json" | "text";
|
|
@@ -31,7 +33,7 @@ export type SessionStartContext = "full" | "without-handoff" | "none";
|
|
|
31
33
|
* is resumed. Copilot does not document it, and has shipped a release that
|
|
32
34
|
* dropped startup context, so it keeps getting the full context on resume.
|
|
33
35
|
*/
|
|
34
|
-
const RESUME_KEEPS_CONTEXT: ReadonlySet<string> = new Set(["claude", "codex"]);
|
|
36
|
+
const RESUME_KEEPS_CONTEXT: ReadonlySet<string> = new Set<AgentName>(["claude", "codex"]);
|
|
35
37
|
|
|
36
38
|
/**
|
|
37
39
|
* After a compaction, CompactRecover restores the last exchange, so the
|
package/src/hooks/lib/stop.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readFileSync } from "node:fs";
|
|
7
|
+
import { antigravityMessage } from "./antigravity-transcript";
|
|
7
8
|
|
|
8
9
|
interface Message {
|
|
9
10
|
role: string;
|
|
@@ -61,7 +62,8 @@ function codexMessage(payload: CodexPayload | undefined): Message | null {
|
|
|
61
62
|
// Claude Code tags transcript lines `type: "user"|"assistant"` with the text
|
|
62
63
|
// under `message.content`. VS Code Copilot's own event log instead uses
|
|
63
64
|
// `type: "user.message"|"assistant.message"` with a flat `data.content`
|
|
64
|
-
// string. Codex nests each message as a `response_item` payload.
|
|
65
|
+
// string. Codex nests each message as a `response_item` payload. Antigravity
|
|
66
|
+
// writes one step per line, `type: "USER_INPUT"|"PLANNER_RESPONSE"|…`.
|
|
65
67
|
function parseTranscriptEntry(entry: {
|
|
66
68
|
type?: string;
|
|
67
69
|
message?: { content?: unknown };
|
|
@@ -78,14 +80,15 @@ function parseTranscriptEntry(entry: {
|
|
|
78
80
|
const role = entry.type === "user.message" ? "user" : "assistant";
|
|
79
81
|
return typeof text === "string" && text ? { role, content: text } : null;
|
|
80
82
|
}
|
|
81
|
-
return
|
|
83
|
+
return antigravityMessage(entry);
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
/**
|
|
85
87
|
* Read an agent transcript JSONL file and extract user/assistant messages.
|
|
86
88
|
* Supports Claude Code's `{type:"user"|"assistant", message:{content}}` shape
|
|
87
89
|
* VS Code Copilot's `{type:"user.message"|"assistant.message", data:{content}}` shape,
|
|
88
|
-
*
|
|
90
|
+
* Codex's `{type:"response_item", payload:{type:"message", role, content}}` shape,
|
|
91
|
+
* and Antigravity's `{type:"USER_INPUT"|"PLANNER_RESPONSE", source, content}` steps.
|
|
89
92
|
*/
|
|
90
93
|
export function readTranscriptFile(path: string): Message[] {
|
|
91
94
|
try {
|
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
type AgentDefinition,
|
|
8
|
-
type AgentPlatform,
|
|
9
8
|
parseAgentDefinition,
|
|
10
9
|
} from "../hooks/lib/agent-definition";
|
|
10
|
+
import type { AgentName } from "../hooks/lib/agent-registry";
|
|
11
11
|
|
|
12
12
|
const CODEX_OWNED_KEYS = new Set(["name", "description", "developer_instructions"]);
|
|
13
13
|
|
|
14
|
-
export function agentFileName(stem: string, platform:
|
|
14
|
+
export function agentFileName(stem: string, platform: AgentName): string {
|
|
15
15
|
return `${stem}.${platform === "codex" ? "toml" : "md"}`;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function renderAgentForPlatform(content: string, platform:
|
|
18
|
+
export function renderAgentForPlatform(content: string, platform: AgentName): string {
|
|
19
19
|
const definition = parseAgentDefinition(content);
|
|
20
20
|
if (platform === "codex") return renderCodexAgent(definition);
|
|
21
21
|
if (!definition.hasFrontmatter) return content;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAL — Antigravity CLI target installer
|
|
3
|
+
* Rules, hooks, skills and subagents live in one plugin, ~/.gemini/config/plugins/pal/.
|
|
4
|
+
* A plugin cannot carry permissions or a status line, so the command allowlist and
|
|
5
|
+
* the statusLine are merged into agy's settings.json; the user's own hooks.json is
|
|
6
|
+
* never touched.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { dirname, resolve } from "node:path";
|
|
11
|
+
import { skillsDirOf } from "../../hooks/lib/agent-registry";
|
|
12
|
+
import { ensureAntigravityRule } from "../../hooks/lib/claude-md";
|
|
13
|
+
import { assets, palPkg, platform } from "../../hooks/lib/paths";
|
|
14
|
+
import {
|
|
15
|
+
addStatuslineConfig,
|
|
16
|
+
copyAgentsForAntigravity,
|
|
17
|
+
copySkills,
|
|
18
|
+
copyStatusline,
|
|
19
|
+
countSkills,
|
|
20
|
+
loadHooksTemplate,
|
|
21
|
+
loadSettingsTemplate,
|
|
22
|
+
log,
|
|
23
|
+
mergeSettings,
|
|
24
|
+
nativeAgentsDir,
|
|
25
|
+
readJson,
|
|
26
|
+
writeJson,
|
|
27
|
+
} from "../lib";
|
|
28
|
+
|
|
29
|
+
const PLUGIN_DIR = platform.antigravityPluginDir();
|
|
30
|
+
|
|
31
|
+
mkdirSync(PLUGIN_DIR, { recursive: true });
|
|
32
|
+
writeJson(resolve(PLUGIN_DIR, "plugin.json"), {
|
|
33
|
+
name: "pal",
|
|
34
|
+
description: "Portable Agent Layer — personal context, skills and hooks",
|
|
35
|
+
});
|
|
36
|
+
log.success(`PAL plugin → ${PLUGIN_DIR}`);
|
|
37
|
+
|
|
38
|
+
ensureAntigravityRule();
|
|
39
|
+
log.success(
|
|
40
|
+
"PAL instructions → rules/pal.md (context rules follow from the shared digests)"
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const PKG_ROOT = palPkg().replaceAll("\\", "/");
|
|
44
|
+
writeJson(
|
|
45
|
+
resolve(PLUGIN_DIR, "hooks.json"),
|
|
46
|
+
loadHooksTemplate(assets.antigravityHooksTemplate(), PKG_ROOT, "Antigravity CLI")
|
|
47
|
+
);
|
|
48
|
+
log.success("PAL hooks → hooks.json");
|
|
49
|
+
|
|
50
|
+
const SETTINGS = platform.antigravitySettings();
|
|
51
|
+
mkdirSync(dirname(SETTINGS), { recursive: true });
|
|
52
|
+
if (existsSync(SETTINGS)) copyFileSync(SETTINGS, `${SETTINGS}.bak.${Date.now()}`);
|
|
53
|
+
writeJson(
|
|
54
|
+
SETTINGS,
|
|
55
|
+
addStatuslineConfig(
|
|
56
|
+
mergeSettings(
|
|
57
|
+
readJson(SETTINGS, {}),
|
|
58
|
+
loadSettingsTemplate(assets.antigravitySettingsTemplate(), PKG_ROOT)
|
|
59
|
+
),
|
|
60
|
+
"antigravity"
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
log.success("PAL command allowlist and statusLine → antigravity-cli/settings.json");
|
|
64
|
+
copyStatusline("antigravity");
|
|
65
|
+
|
|
66
|
+
copySkills(skillsDirOf("antigravity"));
|
|
67
|
+
const agentCount = copyAgentsForAntigravity(nativeAgentsDir("antigravity"));
|
|
68
|
+
log.success(`${countSkills()} skills · ${agentCount} agents → ${PLUGIN_DIR}`);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAL — Antigravity CLI uninstaller
|
|
3
|
+
* Removes the PAL plugin, ~/.gemini/config/plugins/pal/, exactly the allowlist
|
|
4
|
+
* entries PAL merged into agy's settings.json, and PAL's statusLine and script. Skill links are unlinked first so
|
|
5
|
+
* removing the folder never walks into the skills they point at.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
copyFileSync,
|
|
10
|
+
existsSync,
|
|
11
|
+
lstatSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
rmSync,
|
|
14
|
+
unlinkSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { resolve } from "node:path";
|
|
17
|
+
import { skillsDirOf } from "../../hooks/lib/agent-registry";
|
|
18
|
+
import { assets, palPkg, platform } from "../../hooks/lib/paths";
|
|
19
|
+
import {
|
|
20
|
+
loadSettingsTemplate,
|
|
21
|
+
log,
|
|
22
|
+
readJson,
|
|
23
|
+
removeStatusline,
|
|
24
|
+
removeStatuslineConfig,
|
|
25
|
+
unmergeSettings,
|
|
26
|
+
writeJson,
|
|
27
|
+
} from "../lib";
|
|
28
|
+
|
|
29
|
+
function unlinkSkillLinks(dir: string): string[] {
|
|
30
|
+
if (!existsSync(dir)) return [];
|
|
31
|
+
const links = readdirSync(dir).filter((name) =>
|
|
32
|
+
lstatSync(resolve(dir, name)).isSymbolicLink()
|
|
33
|
+
);
|
|
34
|
+
for (const name of links) unlinkSync(resolve(dir, name));
|
|
35
|
+
return links;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PLUGIN_DIR = platform.antigravityPluginDir();
|
|
39
|
+
|
|
40
|
+
if (existsSync(PLUGIN_DIR)) {
|
|
41
|
+
const removed = unlinkSkillLinks(skillsDirOf("antigravity"));
|
|
42
|
+
rmSync(PLUGIN_DIR, { recursive: true, force: true });
|
|
43
|
+
log.success(`Removed the PAL plugin and ${removed.length} skill link(s)`);
|
|
44
|
+
} else {
|
|
45
|
+
log.info("No PAL plugin found");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const SETTINGS = platform.antigravitySettings();
|
|
49
|
+
if (existsSync(SETTINGS)) {
|
|
50
|
+
copyFileSync(SETTINGS, `${SETTINGS}.bak.${Date.now()}`);
|
|
51
|
+
const template = loadSettingsTemplate(
|
|
52
|
+
assets.antigravitySettingsTemplate(),
|
|
53
|
+
palPkg().replaceAll("\\", "/")
|
|
54
|
+
);
|
|
55
|
+
writeJson(
|
|
56
|
+
SETTINGS,
|
|
57
|
+
removeStatuslineConfig(
|
|
58
|
+
unmergeSettings(readJson(SETTINGS, {}), template),
|
|
59
|
+
"antigravity"
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
log.success(
|
|
63
|
+
"Removed the PAL command allowlist and statusLine from antigravity-cli/settings.json"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
removeStatusline("antigravity");
|
package/src/targets/lib.ts
CHANGED
|
@@ -18,8 +18,13 @@ import {
|
|
|
18
18
|
} from "node:fs";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { dirname, isAbsolute, resolve, sep } from "node:path";
|
|
21
|
-
import {
|
|
22
|
-
|
|
21
|
+
import {
|
|
22
|
+
AGENT_NAMES,
|
|
23
|
+
AGENT_REGISTRY,
|
|
24
|
+
type AgentName,
|
|
25
|
+
skillsDirOf,
|
|
26
|
+
} from "../hooks/lib/agent-registry";
|
|
27
|
+
import { agentDirOverrides, assets, palHome, platform } from "../hooks/lib/paths";
|
|
23
28
|
import { declaredTriggers } from "../hooks/lib/skill-triggers";
|
|
24
29
|
import { agentFileName, renderAgentForPlatform } from "./agent-render";
|
|
25
30
|
|
|
@@ -844,20 +849,10 @@ function pruneStaleSkillLinks(agentSkillsDir: string): string[] {
|
|
|
844
849
|
return removed;
|
|
845
850
|
}
|
|
846
851
|
|
|
847
|
-
/**
|
|
848
|
-
* Agent skills directories that need a per-skill discovery link.
|
|
849
|
-
*
|
|
850
|
-
* opencode is intentionally absent: it discovers the whole ~/.pal/skills/ tree
|
|
851
|
-
* via the ~/.agents/skills → ~/.pal/skills symlink, so any personal skill is
|
|
852
|
-
* picked up without a per-skill link.
|
|
853
|
-
*/
|
|
854
852
|
function perSkillAgentDirs(): { agent: string; dir: string }[] {
|
|
855
|
-
return
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
{ agent: "copilot", dir: resolve(platform.copilotDir(), "skills") },
|
|
859
|
-
{ agent: "codex", dir: resolve(platform.codexDir(), "skills") },
|
|
860
|
-
];
|
|
853
|
+
return AGENT_NAMES.filter(
|
|
854
|
+
(agent) => AGENT_REGISTRY[agent].skills === "own-skills-dir"
|
|
855
|
+
).map((agent) => ({ agent, dir: skillsDirOf(agent) }));
|
|
861
856
|
}
|
|
862
857
|
|
|
863
858
|
/**
|
|
@@ -890,15 +885,9 @@ export function linkPersonalSkill(name: string): string[] {
|
|
|
890
885
|
* PAL_*_DIR vars at a sandbox silently rewires their real setup.
|
|
891
886
|
*/
|
|
892
887
|
function realAgentRoots(): string[] {
|
|
893
|
-
const h = homedir();
|
|
894
888
|
return [
|
|
895
|
-
resolve(
|
|
896
|
-
|
|
897
|
-
resolve(h, ".cursor"),
|
|
898
|
-
resolve(h, ".copilot"),
|
|
899
|
-
resolve(h, ".codex"),
|
|
900
|
-
resolve(h, ".agents"),
|
|
901
|
-
resolve(h, ".config", "opencode"),
|
|
889
|
+
resolve(homedir(), ".pal"),
|
|
890
|
+
...agentDirOverrides().map(({ realDir }) => realDir),
|
|
902
891
|
];
|
|
903
892
|
}
|
|
904
893
|
|
|
@@ -917,8 +906,9 @@ function assertInsideTestSandbox(link: string): void {
|
|
|
917
906
|
if (!escaped) return;
|
|
918
907
|
throw new Error(
|
|
919
908
|
`Refusing to write ${link}: outside the test sandbox (${escaped} is a real agent directory). ` +
|
|
920
|
-
|
|
921
|
-
|
|
909
|
+
`Point ${agentDirOverrides()
|
|
910
|
+
.map(({ env }) => env)
|
|
911
|
+
.join(", ")} at a temp directory in this test.`
|
|
922
912
|
);
|
|
923
913
|
}
|
|
924
914
|
|
|
@@ -1044,7 +1034,7 @@ const shippedAgentSource = (stem: string) =>
|
|
|
1044
1034
|
readFileSync(resolve(assets.agents(), `${stem}.md`), "utf-8");
|
|
1045
1035
|
|
|
1046
1036
|
/** Install agents for a platform into a target directory. Always overwrites. */
|
|
1047
|
-
function installAgents(targetDir: string, platform:
|
|
1037
|
+
function installAgents(targetDir: string, platform: AgentName): number {
|
|
1048
1038
|
if (!existsSync(assets.agents())) return 0;
|
|
1049
1039
|
|
|
1050
1040
|
mkdirSync(targetDir, { recursive: true });
|
|
@@ -1064,7 +1054,7 @@ function installAgents(targetDir: string, platform: AgentPlatform): number {
|
|
|
1064
1054
|
}
|
|
1065
1055
|
|
|
1066
1056
|
/** Shipped agents in targetDir that differ from what installAgents would write there. */
|
|
1067
|
-
export function staleShippedAgents(targetDir: string, agent:
|
|
1057
|
+
export function staleShippedAgents(targetDir: string, agent: AgentName): string[] {
|
|
1068
1058
|
if (!existsSync(assets.agents())) return [];
|
|
1069
1059
|
const isStale = (stem: string) => {
|
|
1070
1060
|
const installed = resolve(targetDir, agentFileName(stem, agent));
|
|
@@ -1078,7 +1068,7 @@ export function staleShippedAgents(targetDir: string, agent: AgentPlatform): str
|
|
|
1078
1068
|
}
|
|
1079
1069
|
|
|
1080
1070
|
/** Remove PAL agents from a directory. */
|
|
1081
|
-
function uninstallAgents(targetDir: string, platform:
|
|
1071
|
+
function uninstallAgents(targetDir: string, platform: AgentName): string[] {
|
|
1082
1072
|
if (!existsSync(assets.agents())) return [];
|
|
1083
1073
|
|
|
1084
1074
|
const removed: string[] = [];
|
|
@@ -1113,6 +1103,10 @@ export function copyAgentsForCodex(codexAgentsDir: string): number {
|
|
|
1113
1103
|
return installAgents(codexAgentsDir, "codex");
|
|
1114
1104
|
}
|
|
1115
1105
|
|
|
1106
|
+
export function copyAgentsForAntigravity(pluginAgentsDir: string): number {
|
|
1107
|
+
return installAgents(pluginAgentsDir, "antigravity");
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1116
1110
|
export function removeAgentsFromCursor(cursorAgentsDir: string): string[] {
|
|
1117
1111
|
return uninstallAgents(cursorAgentsDir, "cursor");
|
|
1118
1112
|
}
|
|
@@ -1138,20 +1132,13 @@ const palAgentsStore = () => resolve(palHome(), "agents");
|
|
|
1138
1132
|
* written into. An agent counts as installed when its agents directory already
|
|
1139
1133
|
* exists (mirrors linkPersonalSkill's per-agent gate).
|
|
1140
1134
|
*/
|
|
1141
|
-
function personalSubagentTargets(): { agent:
|
|
1142
|
-
return
|
|
1135
|
+
function personalSubagentTargets(): { agent: AgentName; dir: string }[] {
|
|
1136
|
+
return AGENT_NAMES.map((agent) => ({ agent, dir: nativeAgentsDir(agent) }));
|
|
1143
1137
|
}
|
|
1144
1138
|
|
|
1145
1139
|
/** The directory each agent reads its subagent definitions from. */
|
|
1146
|
-
export function nativeAgentsDir(agent:
|
|
1147
|
-
|
|
1148
|
-
claude: platform.claudeDir,
|
|
1149
|
-
opencode: platform.opencodeDir,
|
|
1150
|
-
cursor: platform.cursorDir,
|
|
1151
|
-
copilot: platform.copilotDir,
|
|
1152
|
-
codex: platform.codexDir,
|
|
1153
|
-
}[agent];
|
|
1154
|
-
return resolve(home(), "agents");
|
|
1140
|
+
export function nativeAgentsDir(agent: AgentName): string {
|
|
1141
|
+
return resolve(AGENT_REGISTRY[agent].home(), "agents");
|
|
1155
1142
|
}
|
|
1156
1143
|
|
|
1157
1144
|
/** Names of the subagents PAL ships (assets/agents/*.md). */
|
|
@@ -1203,7 +1190,7 @@ export function installPersonalSubagent(name: string): string[] {
|
|
|
1203
1190
|
return renders.map(({ agent }) => agent);
|
|
1204
1191
|
}
|
|
1205
1192
|
|
|
1206
|
-
function renderForPersonalSubagent(content: string, agent:
|
|
1193
|
+
function renderForPersonalSubagent(content: string, agent: AgentName): string {
|
|
1207
1194
|
try {
|
|
1208
1195
|
return renderAgentForPlatform(content, agent);
|
|
1209
1196
|
} catch (e) {
|
|
@@ -1211,8 +1198,12 @@ function renderForPersonalSubagent(content: string, agent: AgentPlatform): strin
|
|
|
1211
1198
|
}
|
|
1212
1199
|
}
|
|
1213
1200
|
|
|
1214
|
-
/**
|
|
1215
|
-
export function
|
|
1201
|
+
/** Any agent's hooks template with PKG_ROOT filled in, parsed. */
|
|
1202
|
+
export function loadHooksTemplate(
|
|
1203
|
+
templatePath: string,
|
|
1204
|
+
pkgRoot: string,
|
|
1205
|
+
agentLabel: string
|
|
1206
|
+
): unknown {
|
|
1216
1207
|
const resolved = readFileSync(templatePath, "utf-8").replaceAll(
|
|
1217
1208
|
"{{PKG_ROOT}}",
|
|
1218
1209
|
pkgRoot
|
|
@@ -1220,37 +1211,64 @@ export function loadCopilotHooksTemplate(templatePath: string, pkgRoot: string):
|
|
|
1220
1211
|
try {
|
|
1221
1212
|
return JSON.parse(resolved);
|
|
1222
1213
|
} catch (e) {
|
|
1223
|
-
throw new Error(
|
|
1214
|
+
throw new Error(
|
|
1215
|
+
`Failed to parse ${agentLabel} hooks template at ${templatePath}: ${e}`
|
|
1216
|
+
);
|
|
1224
1217
|
}
|
|
1225
1218
|
}
|
|
1226
1219
|
|
|
1220
|
+
/** Load and resolve the Copilot hooks template, substituting PKG_ROOT */
|
|
1221
|
+
export function loadCopilotHooksTemplate(templatePath: string, pkgRoot: string): unknown {
|
|
1222
|
+
return loadHooksTemplate(templatePath, pkgRoot, "Copilot");
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1227
1225
|
// --- Statusline ---
|
|
1228
1226
|
|
|
1229
|
-
export type StatuslineTarget = "claude" | "cursor"
|
|
1227
|
+
export type StatuslineTarget = Extract<AgentName, "claude" | "cursor" | "antigravity">;
|
|
1228
|
+
|
|
1229
|
+
interface StatuslineSlot {
|
|
1230
|
+
dir: () => string;
|
|
1231
|
+
/** The script path as the agent's own config spells it, without the extension. */
|
|
1232
|
+
script: string;
|
|
1233
|
+
powershellFlags: string;
|
|
1234
|
+
extra?: Record<string, unknown>;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const STATUSLINE: Record<StatuslineTarget, StatuslineSlot> = {
|
|
1238
|
+
claude: {
|
|
1239
|
+
dir: platform.claudeDir,
|
|
1240
|
+
script: "~/.claude/statusline",
|
|
1241
|
+
powershellFlags: "-NoProfile -ExecutionPolicy Bypass",
|
|
1242
|
+
},
|
|
1243
|
+
cursor: {
|
|
1244
|
+
dir: platform.cursorDir,
|
|
1245
|
+
script: "~/.cursor/statusline",
|
|
1246
|
+
powershellFlags: "-NoProfile",
|
|
1247
|
+
extra: { updateIntervalMs: 300, timeoutMs: 2000 },
|
|
1248
|
+
},
|
|
1249
|
+
antigravity: {
|
|
1250
|
+
dir: platform.antigravityCliDir,
|
|
1251
|
+
script: "~/.gemini/antigravity-cli/statusline",
|
|
1252
|
+
powershellFlags: "-NoProfile -ExecutionPolicy Bypass",
|
|
1253
|
+
},
|
|
1254
|
+
};
|
|
1230
1255
|
|
|
1231
1256
|
function statuslineAgentDir(target: StatuslineTarget): string {
|
|
1232
|
-
return target
|
|
1257
|
+
return STATUSLINE[target].dir();
|
|
1233
1258
|
}
|
|
1234
1259
|
|
|
1235
1260
|
function statuslineCommand(target: StatuslineTarget): string {
|
|
1236
|
-
const
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
: "~/.cursor/statusline.sh";
|
|
1241
|
-
}
|
|
1242
|
-
return isPlatformWin32
|
|
1243
|
-
? "powershell -NoProfile -ExecutionPolicy Bypass -File ~/.claude/statusline.ps1"
|
|
1244
|
-
: "~/.claude/statusline.sh";
|
|
1261
|
+
const { script, powershellFlags } = STATUSLINE[target];
|
|
1262
|
+
return process.platform === "win32"
|
|
1263
|
+
? `powershell ${powershellFlags} -File ${script}.ps1`
|
|
1264
|
+
: `${script}.sh`;
|
|
1245
1265
|
}
|
|
1246
1266
|
|
|
1247
1267
|
function isPalStatuslineCommand(cmd: string, target: StatuslineTarget): boolean {
|
|
1248
|
-
return target
|
|
1249
|
-
? cmd.includes(".claude/statusline")
|
|
1250
|
-
: cmd.includes(".cursor/statusline");
|
|
1268
|
+
return cmd.includes(STATUSLINE[target].script.slice(2));
|
|
1251
1269
|
}
|
|
1252
1270
|
|
|
1253
|
-
/** Copy statusline script
|
|
1271
|
+
/** Copy the statusline script into the agent's config dir for the current platform */
|
|
1254
1272
|
export function copyStatusline(target: StatuslineTarget = "claude"): boolean {
|
|
1255
1273
|
const agentDir = statuslineAgentDir(target);
|
|
1256
1274
|
mkdirSync(agentDir, { recursive: true });
|
|
@@ -1282,7 +1300,7 @@ export function copyStatusline(target: StatuslineTarget = "claude"): boolean {
|
|
|
1282
1300
|
}
|
|
1283
1301
|
}
|
|
1284
1302
|
|
|
1285
|
-
/** Remove statusline script from
|
|
1303
|
+
/** Remove the statusline script from the agent's config dir */
|
|
1286
1304
|
export function removeStatusline(target: StatuslineTarget = "claude"): boolean {
|
|
1287
1305
|
const isPlatformWin32 = process.platform === "win32";
|
|
1288
1306
|
const scriptName = isPlatformWin32 ? "statusline.ps1" : "statusline.sh";
|
|
@@ -1333,8 +1351,7 @@ export function addStatuslineConfig(
|
|
|
1333
1351
|
if (!claudeStatuslineNeedsRefresh(cmd)) {
|
|
1334
1352
|
return settings;
|
|
1335
1353
|
}
|
|
1336
|
-
} else if (!isPalStatuslineCommand(cmd,
|
|
1337
|
-
// Cursor: preserve user-defined statusLine commands
|
|
1354
|
+
} else if (!isPalStatuslineCommand(cmd, target)) {
|
|
1338
1355
|
return settings;
|
|
1339
1356
|
}
|
|
1340
1357
|
}
|
|
@@ -1344,13 +1361,13 @@ export function addStatuslineConfig(
|
|
|
1344
1361
|
type: "command",
|
|
1345
1362
|
command,
|
|
1346
1363
|
padding: 2,
|
|
1347
|
-
...
|
|
1364
|
+
...STATUSLINE[target].extra,
|
|
1348
1365
|
};
|
|
1349
1366
|
|
|
1350
1367
|
return settings;
|
|
1351
1368
|
}
|
|
1352
1369
|
|
|
1353
|
-
/** Remove statusLine config from settings (PAL-owned only for
|
|
1370
|
+
/** Remove statusLine config from settings (PAL-owned only, except for Claude) */
|
|
1354
1371
|
export function removeStatuslineConfig(
|
|
1355
1372
|
settings: Record<string, unknown>,
|
|
1356
1373
|
target: StatuslineTarget = "claude"
|
|
@@ -1366,7 +1383,7 @@ export function removeStatuslineConfig(
|
|
|
1366
1383
|
}
|
|
1367
1384
|
|
|
1368
1385
|
const cmd = statusLine.command as string | undefined;
|
|
1369
|
-
if (cmd && isPalStatuslineCommand(cmd,
|
|
1386
|
+
if (cmd && isPalStatuslineCommand(cmd, target)) {
|
|
1370
1387
|
delete settings.statusLine;
|
|
1371
1388
|
}
|
|
1372
1389
|
return settings;
|
|
@@ -8,6 +8,7 @@ import { resolve } from "node:path";
|
|
|
8
8
|
import { palPkg, platform } from "../../hooks/lib/paths";
|
|
9
9
|
import { getSemiStaticSources } from "../../hooks/lib/semi-static";
|
|
10
10
|
import { copyAgentsForOpencode, copySkills, countSkills, log, writeJson } from "../lib";
|
|
11
|
+
import { allowPalHome, palHomePattern } from "./permission";
|
|
11
12
|
|
|
12
13
|
const PKG_ROOT = palPkg();
|
|
13
14
|
const OC_GLOBAL_DIR = platform.opencodeDir();
|
|
@@ -63,4 +64,5 @@ const existingInstructions = Array.isArray(ocConfig.instructions)
|
|
|
63
64
|
? (ocConfig.instructions as string[])
|
|
64
65
|
: [];
|
|
65
66
|
ocConfig.instructions = [...new Set([...existingInstructions, ...staticFiles])];
|
|
67
|
+
ocConfig = allowPalHome(ocConfig, palHomePattern());
|
|
66
68
|
writeFileSync(configPath, `${JSON.stringify(ocConfig, null, 2)}\n`, "utf-8");
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { palHome } from "../../hooks/lib/paths";
|
|
2
|
+
|
|
3
|
+
type OpencodeConfig = Record<string, unknown>;
|
|
4
|
+
type Rules = Record<string, string>;
|
|
5
|
+
|
|
6
|
+
export function palHomePattern(): string {
|
|
7
|
+
return `${palHome().replaceAll("\\", "/")}/**`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function externalDirectoryRules(config: OpencodeConfig): Rules {
|
|
11
|
+
const permission = (config.permission ?? {}) as Record<string, unknown>;
|
|
12
|
+
const rules = permission.external_directory;
|
|
13
|
+
if (typeof rules === "string") return { "*": rules };
|
|
14
|
+
return { ...((rules ?? {}) as Rules) };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function withExternalDirectoryRules(
|
|
18
|
+
config: OpencodeConfig,
|
|
19
|
+
rules: Rules
|
|
20
|
+
): OpencodeConfig {
|
|
21
|
+
const permission = { ...((config.permission ?? {}) as Record<string, unknown>) };
|
|
22
|
+
if (Object.keys(rules).length > 0) permission.external_directory = rules;
|
|
23
|
+
else delete permission.external_directory;
|
|
24
|
+
const { permission: _previous, ...rest } = config;
|
|
25
|
+
return Object.keys(permission).length > 0 ? { ...rest, permission } : rest;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function allowPalHome(config: OpencodeConfig, pattern: string): OpencodeConfig {
|
|
29
|
+
const { [pattern]: _previous, ...rules } = externalDirectoryRules(config);
|
|
30
|
+
return withExternalDirectoryRules(config, { ...rules, [pattern]: "allow" });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function removePalHomeAllow(
|
|
34
|
+
config: OpencodeConfig,
|
|
35
|
+
pattern: string
|
|
36
|
+
): OpencodeConfig {
|
|
37
|
+
const { [pattern]: _removed, ...rules } = externalDirectoryRules(config);
|
|
38
|
+
return withExternalDirectoryRules(config, rules);
|
|
39
|
+
}
|
|
@@ -8,6 +8,7 @@ import { resolve } from "node:path";
|
|
|
8
8
|
import { platform } from "../../hooks/lib/paths";
|
|
9
9
|
import { getSemiStaticSources } from "../../hooks/lib/semi-static";
|
|
10
10
|
import { log, removeAgentsFromOpencode, removePalDocs, removeSkills } from "../lib";
|
|
11
|
+
import { palHomePattern, removePalHomeAllow } from "./permission";
|
|
11
12
|
|
|
12
13
|
const OC_GLOBAL_DIR = platform.opencodeDir() || "";
|
|
13
14
|
|
|
@@ -58,14 +59,14 @@ try {
|
|
|
58
59
|
/* gone */
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
// --- Remove PAL entries from config.json instructions[] ---
|
|
62
|
+
// --- Remove PAL entries from config.json instructions[] and permission ---
|
|
62
63
|
const configPath = resolve(OC_GLOBAL_DIR, "config.json");
|
|
63
64
|
if (existsSync(configPath) && statSync(configPath).size > 0) {
|
|
64
65
|
try {
|
|
65
|
-
const ocConfig =
|
|
66
|
-
string,
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
const ocConfig = removePalHomeAllow(
|
|
67
|
+
JSON.parse(readFileSync(configPath, "utf-8")) as Record<string, unknown>,
|
|
68
|
+
palHomePattern()
|
|
69
|
+
);
|
|
69
70
|
if (Array.isArray(ocConfig.instructions)) {
|
|
70
71
|
const palFiles = new Set(getSemiStaticSources().map((s) => s.path));
|
|
71
72
|
const filtered = (ocConfig.instructions as string[]).filter(
|
|
@@ -76,11 +77,11 @@ if (existsSync(configPath) && statSync(configPath).size > 0) {
|
|
|
76
77
|
} else {
|
|
77
78
|
ocConfig.instructions = filtered;
|
|
78
79
|
}
|
|
79
|
-
writeFileSync(configPath, `${JSON.stringify(ocConfig, null, 2)}\n`, "utf-8");
|
|
80
|
-
log.success("Removed PAL entries from config.json instructions[]");
|
|
81
80
|
}
|
|
81
|
+
writeFileSync(configPath, `${JSON.stringify(ocConfig, null, 2)}\n`, "utf-8");
|
|
82
|
+
log.success("Removed PAL entries from config.json");
|
|
82
83
|
} catch {
|
|
83
|
-
log.warn("Could not clean config.json
|
|
84
|
+
log.warn("Could not clean config.json — check manually");
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
|