portable-agent-layer 0.93.0 → 0.95.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/assets/schema/pal-settings.schema.json +4 -0
- package/assets/skills/research-entity/SKILL.md +57 -0
- package/assets/templates/hooks.antigravity.json +1 -1
- package/assets/templates/hooks.codex.json +11 -1
- package/assets/templates/hooks.copilot.json +11 -4
- package/assets/templates/hooks.cursor.json +8 -2
- package/assets/templates/pal-settings.json +1 -0
- package/assets/templates/settings.claude.json +55 -3
- package/package.json +1 -1
- package/src/cli/content.ts +23 -0
- package/src/cli/events.ts +47 -0
- package/src/cli/infer.ts +6 -0
- package/src/cli/knowledge-research.ts +93 -0
- package/src/cli/knowledge.ts +17 -4
- package/src/cli/tree.ts +4 -0
- package/src/hooks/AttentionEvent.ts +22 -0
- package/src/hooks/LoadContext.ts +2 -2
- package/src/hooks/PostToolOrchestrator.ts +20 -0
- package/src/hooks/PreCompactPersist.ts +4 -2
- package/src/hooks/PreserveTranscripts.ts +13 -0
- package/src/hooks/SessionClose.ts +5 -0
- package/src/hooks/UserPromptOrchestrator.ts +9 -2
- package/src/hooks/handlers/entity-extraction.ts +10 -3
- package/src/hooks/lib/attention-events.ts +43 -0
- package/src/hooks/lib/auto-update.ts +24 -1
- package/src/hooks/lib/commitment-events.ts +20 -0
- package/src/hooks/lib/discord-events.ts +25 -0
- package/src/hooks/lib/entity-events.ts +16 -0
- package/src/hooks/lib/entity-extraction.ts +24 -2
- package/src/hooks/lib/entity-research.ts +305 -0
- package/src/hooks/lib/entity-review.ts +19 -6
- package/src/hooks/lib/events.ts +252 -0
- package/src/hooks/lib/export.ts +1 -1
- package/src/hooks/lib/hook-timing.ts +33 -0
- package/src/hooks/lib/inference-events.ts +56 -0
- package/src/hooks/lib/inference.ts +51 -6
- package/src/hooks/lib/interaction.ts +74 -27
- package/src/hooks/lib/opencode-events.ts +59 -0
- package/src/hooks/lib/paths.ts +1 -0
- package/src/hooks/lib/post-tool.ts +66 -0
- package/src/hooks/lib/session-events.ts +211 -0
- package/src/hooks/lib/settings.ts +6 -0
- package/src/hooks/lib/stop.ts +27 -31
- package/src/hooks/lib/token-usage.ts +1 -0
- package/src/hooks/lib/tool-events.ts +106 -0
- package/src/hooks/lib/transcript-preserve.ts +133 -0
- package/src/hooks/lib/turn-extras.ts +78 -0
- package/src/hooks/lib/waiting-on-user.ts +75 -0
- package/src/hooks/lib/work-tracking.ts +17 -3
- package/src/targets/opencode/plugin.ts +33 -1
- package/src/tools/agent/interaction.ts +2 -2
- package/src/tools/agent/project.ts +4 -0
- package/src/tools/control-room/snooze.ts +2 -0
- package/src/tools/control-room/writes.ts +2 -0
- package/src/tools/knowledge/lib.ts +5 -0
- package/src/hooks/LedgerCommit.ts +0 -34
- package/src/hooks/LedgerUnapplied.ts +0 -47
package/README.md
CHANGED
|
@@ -85,6 +85,8 @@ pal cli status # check your setup
|
|
|
85
85
|
| `pal cli migrate` | Run pending data migrations (non-destructive) |
|
|
86
86
|
| `pal cli analyze [--actionable]` | Learning analysis: rating trends, failure patterns, graduation candidates |
|
|
87
87
|
| `pal cli usage` | Summarize token usage and estimated cost |
|
|
88
|
+
| `pal cli content preserve` | Copy every Claude Code transcript, gzipped, into `memory/content/` before Claude Code deletes it (30 days by default). Runs on its own whenever a session closes; the content store never leaves the machine, so `export` skips it |
|
|
89
|
+
| `pal cli events discord-nag <sent\|answered>` | For a bot outside PAL: record a nag it sent (`--kind <kind>`), or the answer to one and how long it took (`--after-sec <n>`), in the local event log |
|
|
88
90
|
| `pal cli infer` | One model call for scripts and bots, on the active agent's route: prompt on stdin, reply on stdout. `--tier small\|medium`, `--system <file>`, `--schema <file>` for a JSON reply checked against it, `--timeout <seconds>`. The call loads no PAL hooks or context; exits 1 with the reason when no route answers |
|
|
89
91
|
| `pal cli actor [label <name>]` | Show or rename the actor — who caused a record. Travels with an export, so a shared memory can tell two people apart |
|
|
90
92
|
| `pal cli machine [label <name>]` | Show or rename this install — where a record was written. Never leaves the machine |
|
|
@@ -208,7 +210,9 @@ PAL ships with built-in skills that extend your agent's capabilities:
|
|
|
208
210
|
| `presentation` | Build branded slide decks from outlines |
|
|
209
211
|
| `projects` | Look up, resume, register, or manage tracked projects |
|
|
210
212
|
| `reflect` | Diagnose why a PAL behavior didn't trigger |
|
|
211
|
-
| `
|
|
213
|
+
| `research-entity` | Research a company or person on the web: links, registry entry, recent news |
|
|
214
|
+
| `rule-review` | Approve or deny the rules PAL drafted from your corrections |
|
|
215
|
+
| `telos` | Inspect or update goals, beliefs, strategies, narratives |
|
|
212
216
|
| `think` | Structured first-pass reasoning on a problem |
|
|
213
217
|
|
|
214
218
|
---
|
|
@@ -106,6 +106,10 @@
|
|
|
106
106
|
"type": "boolean",
|
|
107
107
|
"description": "After a session stops, ask a background model which people and companies your own messages mentioned, and write clear matches and new full names to the knowledge store with the message as source. A name that could be more than one entry, or a first name alone, waits for `pal cli knowledge review` instead."
|
|
108
108
|
},
|
|
109
|
+
"entityResearch": {
|
|
110
|
+
"type": "boolean",
|
|
111
|
+
"description": "Opt-in, off by default. When extraction creates a company, or a person with a known organization, research it on the web through the active agent (up to three per stop): website, LinkedIn, social accounts, the company registry entry, recent news, each with its source. A profile the model is sure of waits in `pal cli knowledge review`; accepting writes it to the entry. The name and what you said about it reach the agent's web search. Claude Code, Codex and Copilot only. Needs entityExtraction."
|
|
112
|
+
},
|
|
109
113
|
"steeringTestReport": { "type": "boolean", "description": "When true, the assistant notes to the user which steering self-check fired. For the dual-live test period; ships false." }
|
|
110
114
|
}
|
|
111
115
|
},
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: research-entity
|
|
3
|
+
license: MIT
|
|
4
|
+
description: "Research one company or person on the web and build a sourced profile: official website, LinkedIn, social accounts, the company registry entry (cégjegyzék for Hungarian companies), and recent news or statements. Use when the user asks to research, look up, profile, or background-check a specific company or person, or to enrich a knowledge-store entry."
|
|
5
|
+
argument-hint: <company or person name> [what you know about them]
|
|
6
|
+
metadata:
|
|
7
|
+
source: portable-agent-layer
|
|
8
|
+
triggers:
|
|
9
|
+
- "research-entity"
|
|
10
|
+
- "research entity"
|
|
11
|
+
- "research this company"
|
|
12
|
+
- "research this person"
|
|
13
|
+
- "look up the company"
|
|
14
|
+
- "company background"
|
|
15
|
+
- "background check"
|
|
16
|
+
- "cégjegyzék"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
Research the company or person named in $ARGUMENTS and show a sourced profile. The research runs through `pal cli knowledge research`, which picks the active agent's web search and limits it to searching and reading pages. Never research with your own tools instead: the command is what keeps the result in the same shape on every agent.
|
|
20
|
+
|
|
21
|
+
## Workflow
|
|
22
|
+
|
|
23
|
+
1. Work out from $ARGUMENTS and the conversation:
|
|
24
|
+
- the exact name;
|
|
25
|
+
- whether it is a company or a person;
|
|
26
|
+
- for a person, where they work;
|
|
27
|
+
- one sentence of what the user said about them.
|
|
28
|
+
If you cannot tell a company from a person, ask.
|
|
29
|
+
2. Run the research. For a company:
|
|
30
|
+
```bash
|
|
31
|
+
pal cli knowledge research "<name>" --context "<what the user said>"
|
|
32
|
+
```
|
|
33
|
+
For a person, add `--person` and, when known, `--organization "<employer>"`:
|
|
34
|
+
```bash
|
|
35
|
+
pal cli knowledge research "<name>" --person --organization "<employer>" --context "<what the user said>"
|
|
36
|
+
```
|
|
37
|
+
It takes one to several minutes. Add `--queue` when the user wants the profile saved; it then waits in the review queue.
|
|
38
|
+
3. If the command exits 1, tell the user the reason it printed. Usual reasons:
|
|
39
|
+
- the model found only namesakes (`match: unsure`): ask for something that tells them apart, such as an employer, a city or a website, and run it again with that in `--context` or `--organization`;
|
|
40
|
+
- nothing was found (`match: none`);
|
|
41
|
+
- the active agent cannot search the web yet.
|
|
42
|
+
4. If it succeeds, show the profile it printed as it is. Do not add facts it did not print.
|
|
43
|
+
5. If you queued it, tell the user the review command it printed: `pal cli knowledge review accept <id>` writes the profile to the entry, and `pal cli knowledge review reject <id>` drops it.
|
|
44
|
+
|
|
45
|
+
## Output format
|
|
46
|
+
|
|
47
|
+
The profile as printed: summary, links, registry, news and sources. Then one line saying whether it was queued for review, with the accept command.
|
|
48
|
+
|
|
49
|
+
## When to use
|
|
50
|
+
|
|
51
|
+
- The user names a specific company or person and wants to know who they are, or their website, LinkedIn, accounts, registry data or recent news.
|
|
52
|
+
- The user wants an entry in the knowledge store filled in from the web.
|
|
53
|
+
|
|
54
|
+
## Do NOT use
|
|
55
|
+
|
|
56
|
+
- To pull every person and company out of an article, video or paste. Use `entities` for that.
|
|
57
|
+
- For a broad topic, market or question rather than one named entity. Use `deep-research` for that.
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"hooks": [
|
|
40
40
|
{
|
|
41
41
|
"type": "command",
|
|
42
|
-
"command": "bun run {{PKG_ROOT}}/src/hooks/
|
|
42
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=codex"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
45
45
|
}
|
|
@@ -54,6 +54,16 @@
|
|
|
54
54
|
]
|
|
55
55
|
}
|
|
56
56
|
],
|
|
57
|
+
"PreCompact": [
|
|
58
|
+
{
|
|
59
|
+
"hooks": [
|
|
60
|
+
{
|
|
61
|
+
"type": "command",
|
|
62
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PreCompactPersist.ts --agent=codex"
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
],
|
|
57
67
|
"SessionEnd": [
|
|
58
68
|
{
|
|
59
69
|
"hooks": [
|
|
@@ -47,15 +47,15 @@
|
|
|
47
47
|
"postToolUse": [
|
|
48
48
|
{
|
|
49
49
|
"type": "command",
|
|
50
|
-
"bash": "bun run {{PKG_ROOT}}/src/hooks/
|
|
51
|
-
"powershell": "bun run {{PKG_ROOT}}/src/hooks/
|
|
50
|
+
"bash": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=copilot",
|
|
51
|
+
"powershell": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=copilot"
|
|
52
52
|
}
|
|
53
53
|
],
|
|
54
54
|
"postToolUseFailure": [
|
|
55
55
|
{
|
|
56
56
|
"type": "command",
|
|
57
|
-
"bash": "bun run {{PKG_ROOT}}/src/hooks/
|
|
58
|
-
"powershell": "bun run {{PKG_ROOT}}/src/hooks/
|
|
57
|
+
"bash": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=copilot --event=postToolUseFailure",
|
|
58
|
+
"powershell": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=copilot --event=postToolUseFailure"
|
|
59
59
|
}
|
|
60
60
|
],
|
|
61
61
|
"agentStop": [
|
|
@@ -65,6 +65,13 @@
|
|
|
65
65
|
"powershell": "bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts --agent=copilot"
|
|
66
66
|
}
|
|
67
67
|
],
|
|
68
|
+
"preCompact": [
|
|
69
|
+
{
|
|
70
|
+
"type": "command",
|
|
71
|
+
"bash": "bun run {{PKG_ROOT}}/src/hooks/PreCompactPersist.ts --agent=copilot",
|
|
72
|
+
"powershell": "bun run {{PKG_ROOT}}/src/hooks/PreCompactPersist.ts --agent=copilot"
|
|
73
|
+
}
|
|
74
|
+
],
|
|
68
75
|
"sessionEnd": [
|
|
69
76
|
{
|
|
70
77
|
"type": "command",
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
"postToolUse": [
|
|
35
35
|
{
|
|
36
36
|
"type": "command",
|
|
37
|
-
"command": "bun run {{PKG_ROOT}}/src/hooks/
|
|
37
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=cursor"
|
|
38
38
|
}
|
|
39
39
|
],
|
|
40
40
|
"postToolUseFailure": [
|
|
41
41
|
{
|
|
42
42
|
"type": "command",
|
|
43
|
-
"command": "bun run {{PKG_ROOT}}/src/hooks/
|
|
43
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=cursor"
|
|
44
44
|
}
|
|
45
45
|
],
|
|
46
46
|
"beforeShellExecution": [
|
|
@@ -61,6 +61,12 @@
|
|
|
61
61
|
"command": "bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts --agent=cursor"
|
|
62
62
|
}
|
|
63
63
|
],
|
|
64
|
+
"preCompact": [
|
|
65
|
+
{
|
|
66
|
+
"type": "command",
|
|
67
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PreCompactPersist.ts --agent=cursor"
|
|
68
|
+
}
|
|
69
|
+
],
|
|
64
70
|
"sessionEnd": [
|
|
65
71
|
{
|
|
66
72
|
"type": "command",
|
|
@@ -158,7 +158,17 @@
|
|
|
158
158
|
"hooks": [
|
|
159
159
|
{
|
|
160
160
|
"type": "command",
|
|
161
|
-
"command": "bun run {{PKG_ROOT}}/src/hooks/
|
|
161
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=claude --only=ledger"
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"matcher": "",
|
|
167
|
+
"hooks": [
|
|
168
|
+
{
|
|
169
|
+
"type": "command",
|
|
170
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=claude --only=events",
|
|
171
|
+
"async": true
|
|
162
172
|
}
|
|
163
173
|
]
|
|
164
174
|
}
|
|
@@ -169,7 +179,39 @@
|
|
|
169
179
|
"hooks": [
|
|
170
180
|
{
|
|
171
181
|
"type": "command",
|
|
172
|
-
"command": "bun run {{PKG_ROOT}}/src/hooks/
|
|
182
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=claude --only=ledger"
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"matcher": "",
|
|
188
|
+
"hooks": [
|
|
189
|
+
{
|
|
190
|
+
"type": "command",
|
|
191
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=claude --only=events",
|
|
192
|
+
"async": true
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
"PermissionRequest": [
|
|
198
|
+
{
|
|
199
|
+
"matcher": "",
|
|
200
|
+
"hooks": [
|
|
201
|
+
{
|
|
202
|
+
"type": "command",
|
|
203
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/AttentionEvent.ts --agent=claude"
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
],
|
|
208
|
+
"Notification": [
|
|
209
|
+
{
|
|
210
|
+
"matcher": "",
|
|
211
|
+
"hooks": [
|
|
212
|
+
{
|
|
213
|
+
"type": "command",
|
|
214
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/AttentionEvent.ts --agent=claude"
|
|
173
215
|
}
|
|
174
216
|
]
|
|
175
217
|
}
|
|
@@ -180,7 +222,17 @@
|
|
|
180
222
|
"hooks": [
|
|
181
223
|
{
|
|
182
224
|
"type": "command",
|
|
183
|
-
"command": "bun run {{PKG_ROOT}}/src/hooks/
|
|
225
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=claude --only=ledger"
|
|
226
|
+
}
|
|
227
|
+
]
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"matcher": "",
|
|
231
|
+
"hooks": [
|
|
232
|
+
{
|
|
233
|
+
"type": "command",
|
|
234
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PostToolOrchestrator.ts --agent=claude --only=events",
|
|
235
|
+
"async": true
|
|
184
236
|
}
|
|
185
237
|
]
|
|
186
238
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { preserveTranscriptsOnce } from "../hooks/lib/transcript-preserve";
|
|
2
|
+
import { group, leaf } from "../tools/lib/command";
|
|
3
|
+
|
|
4
|
+
function printPreserve(): undefined {
|
|
5
|
+
const result = preserveTranscriptsOnce();
|
|
6
|
+
if (!result) {
|
|
7
|
+
console.log("Another copy is already running.");
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
console.log(
|
|
11
|
+
`Copied ${result.copied}, unchanged ${result.unchanged}, failed ${result.failed}`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const contentCommand = group({
|
|
16
|
+
summary: "PAL's local content store (never exported)",
|
|
17
|
+
commands: {
|
|
18
|
+
preserve: leaf({
|
|
19
|
+
summary: "Copy Claude Code transcripts, gzipped, before Claude Code deletes them",
|
|
20
|
+
run: printPreserve,
|
|
21
|
+
}),
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { isNagStage, logDiscordNag, NAG_STAGES } from "../hooks/lib/discord-events";
|
|
2
|
+
import { group, leaf, UsageError } from "../tools/lib/command";
|
|
3
|
+
|
|
4
|
+
function secondsFrom(raw: string | undefined): number | null {
|
|
5
|
+
if (raw === undefined) return null;
|
|
6
|
+
const seconds = Number(raw);
|
|
7
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
8
|
+
throw new UsageError(`--after-sec must be a number of seconds, got "${raw}"`);
|
|
9
|
+
}
|
|
10
|
+
return Math.round(seconds);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function recordDiscordNag(
|
|
14
|
+
stage: string | undefined,
|
|
15
|
+
kind: string | undefined,
|
|
16
|
+
afterSec: string | undefined
|
|
17
|
+
): undefined {
|
|
18
|
+
if (!isNagStage(stage)) {
|
|
19
|
+
throw new UsageError(`the stage is one of ${NAG_STAGES.join(", ")}`);
|
|
20
|
+
}
|
|
21
|
+
const responseSec = secondsFrom(afterSec);
|
|
22
|
+
if (stage === "answered" && responseSec === null) {
|
|
23
|
+
throw new UsageError("an answered nag needs --after-sec");
|
|
24
|
+
}
|
|
25
|
+
const id = logDiscordNag(stage, kind ?? null, responseSec);
|
|
26
|
+
console.log(id ? `Recorded ${id}` : "The event log is off; nothing recorded.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const eventsCommand = group({
|
|
30
|
+
summary: "PAL's local event log",
|
|
31
|
+
commands: {
|
|
32
|
+
"discord-nag": leaf({
|
|
33
|
+
summary: "Record a nag the Discord bot sent, or how long its answer took",
|
|
34
|
+
args: "<sent|answered>",
|
|
35
|
+
options: {
|
|
36
|
+
kind: { type: "string", value: "<kind>", description: "What the nag was about" },
|
|
37
|
+
"after-sec": {
|
|
38
|
+
type: "string",
|
|
39
|
+
value: "<n>",
|
|
40
|
+
description: "Seconds from the nag to the answer",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
run: ({ positionals, values }) =>
|
|
44
|
+
recordDiscordNag(positionals[0], values.kind, values["after-sec"]),
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
47
|
+
});
|
package/src/cli/infer.ts
CHANGED
|
@@ -18,6 +18,7 @@ interface InferValues {
|
|
|
18
18
|
schema?: string;
|
|
19
19
|
timeout?: string;
|
|
20
20
|
caller?: string;
|
|
21
|
+
web?: boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
type Infer = typeof inference;
|
|
@@ -81,6 +82,7 @@ function requestFrom(values: InferValues, stdin: string): Parameters<Infer>[0] {
|
|
|
81
82
|
tier: tierOf(values.tier),
|
|
82
83
|
timeout: timeoutMs(values.timeout),
|
|
83
84
|
caller: values.caller ?? "cli-infer",
|
|
85
|
+
web: values.web ?? false,
|
|
84
86
|
};
|
|
85
87
|
}
|
|
86
88
|
|
|
@@ -137,6 +139,10 @@ export const inferCommand = leaf({
|
|
|
137
139
|
value: "<label>",
|
|
138
140
|
description: "Label for PAL's debug log",
|
|
139
141
|
},
|
|
142
|
+
web: {
|
|
143
|
+
type: "boolean",
|
|
144
|
+
description: "Let the model search and read the web (Claude, Codex, Copilot)",
|
|
145
|
+
},
|
|
140
146
|
},
|
|
141
147
|
run: async ({ values }) => inferFromCli(values, await Bun.stdin.text()),
|
|
142
148
|
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pal cli knowledge research — research one person or company on the web through
|
|
3
|
+
* the active agent, print the profile, and optionally queue it for review.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { loadKnownEntities, loadNameIndex } from "../hooks/lib/entity-cards";
|
|
7
|
+
import { queueForReview } from "../hooks/lib/entity-extraction";
|
|
8
|
+
import { lookupName } from "../hooks/lib/entity-names";
|
|
9
|
+
import {
|
|
10
|
+
type ResearchDeps,
|
|
11
|
+
type ResearchTarget,
|
|
12
|
+
renderProfile,
|
|
13
|
+
researchEntity,
|
|
14
|
+
researchItem,
|
|
15
|
+
} from "../hooks/lib/entity-research";
|
|
16
|
+
import { inference } from "../hooks/lib/inference";
|
|
17
|
+
import { identity } from "../hooks/lib/settings";
|
|
18
|
+
import { leaf, UsageError } from "../tools/lib/command";
|
|
19
|
+
|
|
20
|
+
interface ResearchValues {
|
|
21
|
+
person?: boolean;
|
|
22
|
+
organization?: string;
|
|
23
|
+
context?: string;
|
|
24
|
+
queue?: boolean;
|
|
25
|
+
json?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function storedSlug(name: string, kind: ResearchTarget["kind"]): string {
|
|
29
|
+
const domain = kind === "person" ? "People" : "Companies";
|
|
30
|
+
const { entities, exact } = lookupName(name, loadNameIndex(loadKnownEntities()));
|
|
31
|
+
const matches = entities.filter((e) => e.domain === domain);
|
|
32
|
+
return exact && matches.length === 1 ? matches[0].slug : "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function targetFrom(name: string, values: ResearchValues): ResearchTarget {
|
|
36
|
+
const kind = values.person ? "person" : "company";
|
|
37
|
+
return {
|
|
38
|
+
kind,
|
|
39
|
+
name,
|
|
40
|
+
slug: storedSlug(name, kind),
|
|
41
|
+
organization: values.organization ?? "",
|
|
42
|
+
fact: values.context ?? "",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function researchFromCli(
|
|
47
|
+
name: string | undefined,
|
|
48
|
+
values: ResearchValues,
|
|
49
|
+
deps: ResearchDeps = { principal: identity().principal.name, infer: inference }
|
|
50
|
+
): Promise<number> {
|
|
51
|
+
if (!name?.trim()) throw new UsageError("name the person or company to research");
|
|
52
|
+
const target = targetFrom(name.trim(), values);
|
|
53
|
+
const outcome = await researchEntity(target, deps);
|
|
54
|
+
if (!outcome.found) {
|
|
55
|
+
console.error(`Nothing kept for "${target.name}": ${outcome.reason}`);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
console.log(
|
|
59
|
+
values.json
|
|
60
|
+
? JSON.stringify(outcome.profile, null, 2)
|
|
61
|
+
: renderProfile(outcome.profile)
|
|
62
|
+
);
|
|
63
|
+
if (values.queue) {
|
|
64
|
+
const item = researchItem(target, outcome.profile);
|
|
65
|
+
queueForReview([item]);
|
|
66
|
+
console.log(`\nQueued for review: pal cli knowledge review accept ${item.id}`);
|
|
67
|
+
}
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const researchCommand = leaf({
|
|
72
|
+
summary: "Research a company (or --person) on the web: links, registry, news",
|
|
73
|
+
args: "<name>",
|
|
74
|
+
options: {
|
|
75
|
+
person: { type: "boolean", description: "Research a person, not a company" },
|
|
76
|
+
organization: {
|
|
77
|
+
type: "string",
|
|
78
|
+
value: "<name>",
|
|
79
|
+
description: "Where the person works, to tell namesakes apart",
|
|
80
|
+
},
|
|
81
|
+
context: {
|
|
82
|
+
type: "string",
|
|
83
|
+
value: "<text>",
|
|
84
|
+
description: "What you know about them, to tell namesakes apart",
|
|
85
|
+
},
|
|
86
|
+
queue: {
|
|
87
|
+
type: "boolean",
|
|
88
|
+
description: "Also queue the profile for `pal cli knowledge review`",
|
|
89
|
+
},
|
|
90
|
+
json: { type: "boolean", description: "Print the profile as JSON" },
|
|
91
|
+
},
|
|
92
|
+
run: ({ positionals, values }) => researchFromCli(positionals.join(" "), values),
|
|
93
|
+
});
|
package/src/cli/knowledge.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
9
|
import type { ReviewItem } from "../hooks/lib/entity-extraction";
|
|
10
|
+
import { renderProfile } from "../hooks/lib/entity-research";
|
|
10
11
|
import {
|
|
11
12
|
type AcceptChoice,
|
|
12
13
|
acceptReview,
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
type Status,
|
|
35
36
|
} from "../tools/knowledge/lib";
|
|
36
37
|
import { group, leaf, runCommand, UsageError } from "../tools/lib/command";
|
|
38
|
+
import { researchCommand } from "./knowledge-research";
|
|
37
39
|
|
|
38
40
|
const VOCABULARY = `Domains: ${DOMAINS.join(", ")}
|
|
39
41
|
Relation types: ${RELATION_TYPES.join(", ")}`;
|
|
@@ -120,6 +122,7 @@ export const knowledgeCommand = group({
|
|
|
120
122
|
},
|
|
121
123
|
run: ({ values }) => cmdIngest(values.source ?? "manual", values.file),
|
|
122
124
|
}),
|
|
125
|
+
research: researchCommand,
|
|
123
126
|
review: group({
|
|
124
127
|
summary: "People and companies the background extractor would not guess",
|
|
125
128
|
fallback: "list",
|
|
@@ -623,17 +626,27 @@ function reviewReason(reason: ReviewItem["reason"]): string {
|
|
|
623
626
|
ambiguous: "could be more than one known entry",
|
|
624
627
|
"unknown-existing": "named an entry the store does not have",
|
|
625
628
|
"first-name-only": "first name only",
|
|
629
|
+
"web-profile": "found on the web",
|
|
626
630
|
};
|
|
627
631
|
return reasons[reason];
|
|
628
632
|
}
|
|
629
633
|
|
|
630
|
-
function
|
|
631
|
-
|
|
634
|
+
function reviewDetails(item: ReviewItem): string[] {
|
|
635
|
+
if (item.profile)
|
|
636
|
+
return [
|
|
637
|
+
item.entity.existing ? `for ${item.entity.existing}` : "as a new entry",
|
|
638
|
+
...renderProfile(item.profile).split("\n"),
|
|
639
|
+
];
|
|
632
640
|
const candidates =
|
|
633
|
-
item.candidates.length > 0 ? `could be: ${item.candidates.join(", ")}` :
|
|
641
|
+
item.candidates.length > 0 ? [`could be: ${item.candidates.join(", ")}`] : [];
|
|
642
|
+
return [item.entity.fact, ...candidates, `from ${item.source}`];
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function reviewLines(item: ReviewItem): string[] {
|
|
646
|
+
const { kind, name } = item.entity;
|
|
634
647
|
return [
|
|
635
648
|
` ${item.id} ${kind} "${name}" — ${reviewReason(item.reason)}`,
|
|
636
|
-
...
|
|
649
|
+
...reviewDetails(item)
|
|
637
650
|
.filter(Boolean)
|
|
638
651
|
.map((line) => ` ${line}`),
|
|
639
652
|
];
|
package/src/cli/tree.ts
CHANGED
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
type OptionSpecs,
|
|
15
15
|
} from "../tools/lib/command";
|
|
16
16
|
import { builtinTools, builtinToolVerbs } from "./builtin-tools";
|
|
17
|
+
import { contentCommand } from "./content";
|
|
18
|
+
import { eventsCommand } from "./events";
|
|
17
19
|
import { identityCommand } from "./identity";
|
|
18
20
|
import { inferCommand } from "./infer";
|
|
19
21
|
import { knowledgeCommand } from "./knowledge";
|
|
@@ -151,6 +153,8 @@ export function cliTree(admin: AdminHandlers): Group {
|
|
|
151
153
|
commands: {
|
|
152
154
|
...adminCommands(admin),
|
|
153
155
|
usage: usageCommand,
|
|
156
|
+
content: contentCommand,
|
|
157
|
+
events: eventsCommand,
|
|
154
158
|
infer: inferCommand,
|
|
155
159
|
actor: identityCommand("actor"),
|
|
156
160
|
machine: identityCommand("machine"),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook: PermissionRequest and Notification — logs that the agent is waiting on
|
|
3
|
+
* the user. Logging only: it prints nothing, so it never answers the prompt, and
|
|
4
|
+
* always exits 0.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { logAttention } from "./lib/attention-events";
|
|
8
|
+
import { duplicatesCursorHooks } from "./lib/cursor-shadow";
|
|
9
|
+
import { enterHookWorkspace } from "./lib/hook-turn";
|
|
10
|
+
import { isPalSpawnedInference } from "./lib/spawn-guard";
|
|
11
|
+
import { readStdinJSON } from "./lib/stdin";
|
|
12
|
+
|
|
13
|
+
if (isPalSpawnedInference()) process.exit(0);
|
|
14
|
+
if (duplicatesCursorHooks()) process.exit(0);
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const input = await readStdinJSON<Record<string, unknown>>();
|
|
18
|
+
enterHookWorkspace(input);
|
|
19
|
+
logAttention(input);
|
|
20
|
+
} catch {
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
package/src/hooks/LoadContext.ts
CHANGED
|
@@ -24,11 +24,10 @@ import {
|
|
|
24
24
|
isSubagentSession,
|
|
25
25
|
needsAgentsMd,
|
|
26
26
|
} from "./lib/session-context";
|
|
27
|
+
import { logSessionStart } from "./lib/session-events";
|
|
27
28
|
import { isPalSpawnedInference } from "./lib/spawn-guard";
|
|
28
29
|
import { readStdinJSON } from "./lib/stdin";
|
|
29
30
|
|
|
30
|
-
// Recursion guard — when this process is a PAL-spawned inference subprocess,
|
|
31
|
-
// skip all context loading so we don't trigger another inference call.
|
|
32
31
|
if (isPalSpawnedInference()) process.exit(0);
|
|
33
32
|
if (duplicatesCursorHooks()) process.exit(0);
|
|
34
33
|
if (isSubagentSession(process.env)) {
|
|
@@ -51,6 +50,7 @@ try {
|
|
|
51
50
|
try {
|
|
52
51
|
const active = getActiveAgent();
|
|
53
52
|
const input = await readStdinJSON<{ source?: string }>();
|
|
53
|
+
logSessionStart(input);
|
|
54
54
|
const wanted = contextForSource(active, input?.source);
|
|
55
55
|
if (wanted === "none") {
|
|
56
56
|
logDebug("LoadContext", `source=${input?.source}, context already present`);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook: after a tool call, a failed call or a denied one. Silent and fail-open:
|
|
3
|
+
* it prints nothing and always exits 0, so it can never disturb a turn.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { duplicatesCursorHooks } from "./lib/cursor-shadow";
|
|
7
|
+
import { enterHookWorkspace } from "./lib/hook-turn";
|
|
8
|
+
import { runPostToolHandlers } from "./lib/post-tool";
|
|
9
|
+
import { readStdinJSON } from "./lib/stdin";
|
|
10
|
+
|
|
11
|
+
if (duplicatesCursorHooks()) process.exit(0);
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const input = await readStdinJSON<Record<string, unknown>>();
|
|
15
|
+
if (!input) process.exit(0);
|
|
16
|
+
enterHookWorkspace(input);
|
|
17
|
+
runPostToolHandlers(input);
|
|
18
|
+
} catch {
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Storage: ~/.pal/memory/state/last-exchange/{session_id}.json + latest.json.
|
|
9
9
|
* Path lives under PAL_HOME so other agents (OpenCode, Copilot, Cursor) can read the
|
|
10
|
-
* same state if/when they grow equivalent compaction-recovery hooks.
|
|
11
|
-
*
|
|
10
|
+
* same state if/when they grow equivalent compaction-recovery hooks. Cursor, Copilot
|
|
11
|
+
* and Codex run it too, which logs their compactions.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { existsSync } from "node:fs";
|
|
@@ -19,6 +19,7 @@ import { duplicatesCursorHooks } from "./lib/cursor-shadow";
|
|
|
19
19
|
import { logDebug, logError } from "./lib/log";
|
|
20
20
|
import { paths } from "./lib/paths";
|
|
21
21
|
import { sessionDir } from "./lib/session-dir";
|
|
22
|
+
import { logCompaction } from "./lib/session-events";
|
|
22
23
|
import { isPalSpawnedInference } from "./lib/spawn-guard";
|
|
23
24
|
import { readStdinJSON } from "./lib/stdin";
|
|
24
25
|
import { readTranscriptFile } from "./lib/transcript";
|
|
@@ -37,6 +38,7 @@ interface PreCompactInput {
|
|
|
37
38
|
|
|
38
39
|
const main = async () => {
|
|
39
40
|
const input = await readStdinJSON<PreCompactInput>();
|
|
41
|
+
logCompaction(input);
|
|
40
42
|
if (!input?.transcript_path) {
|
|
41
43
|
logDebug("PreCompactPersist", "No transcript_path in input — skipping");
|
|
42
44
|
process.exit(0);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawned, never awaited — copies Claude Code transcripts into PAL's content
|
|
3
|
+
* store in a process of its own, so no session waits on the gzip.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { logError } from "./lib/log";
|
|
7
|
+
import { preserveTranscriptsOnce } from "./lib/transcript-preserve";
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
preserveTranscriptsOnce();
|
|
11
|
+
} catch (err) {
|
|
12
|
+
logError("PreserveTranscripts", err);
|
|
13
|
+
}
|