@nettee/beacon 0.2.3 → 0.2.4
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 +16 -9
- package/dist/config/profile.js +68 -6
- package/examples/profiles/example/profile.yaml +1 -1
- package/package.json +1 -1
- /package/examples/{profiles/example → workspace/.beacon-profile}/persona.md +0 -0
- /package/examples/{profiles/example → workspace/.beacon-profile}/task.md +0 -0
package/README.md
CHANGED
|
@@ -50,18 +50,25 @@ Copy the files under [`examples`](examples) into a private Beacon home and repla
|
|
|
50
50
|
├── config.yaml
|
|
51
51
|
├── runtime.env
|
|
52
52
|
├── secrets.json
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
├── profiles/
|
|
54
|
+
│ └── example/
|
|
55
|
+
│ └── profile.yaml
|
|
56
|
+
└── workspace/
|
|
57
|
+
└── .beacon-profile/
|
|
56
58
|
├── persona.md
|
|
57
59
|
└── task.md
|
|
58
60
|
```
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`prompt.md`.
|
|
62
|
+
yaml names the workspace directory only. Beacon then loads `persona.md`
|
|
63
|
+
(identity) and `task.md` (process) as a pair: `{workspace}/.beacon-profile/`
|
|
64
|
+
first; if that pair is missing, `{profileDir}/`. It never mixes the two
|
|
65
|
+
places, never reads yaml `prompt:` / persona / task paths, and never falls
|
|
66
|
+
back to `prompt.md`. If neither place has a complete pair, Profile load fails
|
|
67
|
+
and lists the missing paths. Both pairs may exist; the workspace pair wins.
|
|
68
|
+
|
|
69
|
+
Beacon prepends an English platform template for this Run: workspace limit,
|
|
70
|
+
inbound vs Schedule vs manual capabilities, and the `reply` / `no_reply` /
|
|
71
|
+
`notify_card` contract.
|
|
65
72
|
|
|
66
73
|
Protect the home and secret file before running Beacon:
|
|
67
74
|
|
|
@@ -102,7 +109,7 @@ created by Beacon 0.1.2 and earlier; when omitted it defaults to `sessions/`
|
|
|
102
109
|
beside `config.yaml`. When configured, it must be absolute. Beacon creates
|
|
103
110
|
per-Run directories with mode `0700` when Pi starts.
|
|
104
111
|
|
|
105
|
-
Configuration is strict: unknown YAML/JSON fields, YAML aliases or warnings, missing paths, duplicate Schedule IDs, invalid timezones/cron expressions, missing `persona.md`
|
|
112
|
+
Configuration is strict: unknown YAML/JSON fields, YAML aliases or warnings, missing paths, duplicate Schedule IDs, invalid timezones/cron expressions, a missing complete `persona.md` + `task.md` pair, escaped Profile markdown, permissive secret or runtime-environment permissions, and missing Profile credentials all fail startup. Beacon validates all Profiles before opening a Feishu connection.
|
|
106
113
|
|
|
107
114
|
Each Schedule uses a five-field cron expression and an IANA timezone. Profile
|
|
108
115
|
`admin.chat_id` is the private chat used for schedule `reply` / `no_reply` and
|
package/dist/config/profile.js
CHANGED
|
@@ -7,6 +7,7 @@ import { parseStrictYaml } from "./yaml.js";
|
|
|
7
7
|
const profileIdPattern = /^[a-z0-9](?:[a-z0-9_-]{0,62})$/;
|
|
8
8
|
export const profilePersonaFile = "persona.md";
|
|
9
9
|
export const profileTaskFile = "task.md";
|
|
10
|
+
export const workspaceProfileDirectoryName = ".beacon-profile";
|
|
10
11
|
const scheduleSchema = z
|
|
11
12
|
.object({
|
|
12
13
|
id: z.string().regex(profileIdPattern),
|
|
@@ -61,8 +62,37 @@ function isWithin(parent, child) {
|
|
|
61
62
|
const path = relative(parent, child);
|
|
62
63
|
return path === "" || (!path.startsWith("..") && !isAbsolute(path));
|
|
63
64
|
}
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
export function workspaceProfileDirectory(workspace) {
|
|
66
|
+
return join(workspace, workspaceProfileDirectoryName);
|
|
67
|
+
}
|
|
68
|
+
async function isExistingFile(path) {
|
|
69
|
+
try {
|
|
70
|
+
return (await stat(path)).isFile();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function markdownPairPaths(directory) {
|
|
77
|
+
return {
|
|
78
|
+
persona: join(directory, profilePersonaFile),
|
|
79
|
+
task: join(directory, profileTaskFile),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async function missingMarkdownPaths(directory) {
|
|
83
|
+
const paths = markdownPairPaths(directory);
|
|
84
|
+
const missing = [];
|
|
85
|
+
if (!(await isExistingFile(paths.persona)))
|
|
86
|
+
missing.push(paths.persona);
|
|
87
|
+
if (!(await isExistingFile(paths.task)))
|
|
88
|
+
missing.push(paths.task);
|
|
89
|
+
return missing;
|
|
90
|
+
}
|
|
91
|
+
async function markdownPairExists(directory) {
|
|
92
|
+
return (await missingMarkdownPaths(directory)).length === 0;
|
|
93
|
+
}
|
|
94
|
+
async function loadRequiredMarkdown(canonicalDirectory, filename) {
|
|
95
|
+
const filePath = join(canonicalDirectory, filename);
|
|
66
96
|
let canonicalFilePath;
|
|
67
97
|
try {
|
|
68
98
|
canonicalFilePath = await realpath(filePath);
|
|
@@ -72,8 +102,8 @@ async function loadRequiredMarkdown(canonicalProfileDirectory, filename) {
|
|
|
72
102
|
cause: error,
|
|
73
103
|
});
|
|
74
104
|
}
|
|
75
|
-
if (!isWithin(
|
|
76
|
-
throw new Error(`Profile ${filename} must stay inside ${
|
|
105
|
+
if (!isWithin(canonicalDirectory, canonicalFilePath)) {
|
|
106
|
+
throw new Error(`Profile ${filename} must stay inside ${canonicalDirectory}`);
|
|
77
107
|
}
|
|
78
108
|
const text = (await readFile(canonicalFilePath, "utf8")).trim();
|
|
79
109
|
if (!text) {
|
|
@@ -81,6 +111,39 @@ async function loadRequiredMarkdown(canonicalProfileDirectory, filename) {
|
|
|
81
111
|
}
|
|
82
112
|
return text;
|
|
83
113
|
}
|
|
114
|
+
async function loadMarkdownPair(directory, boundary) {
|
|
115
|
+
const canonicalBoundary = await realpath(boundary);
|
|
116
|
+
let canonicalDirectory;
|
|
117
|
+
try {
|
|
118
|
+
canonicalDirectory = await realpath(directory);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
throw new Error(`Cannot read Profile markdown at ${directory}`, {
|
|
122
|
+
cause: error,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (!isWithin(canonicalBoundary, canonicalDirectory)) {
|
|
126
|
+
throw new Error(`Profile markdown must stay inside ${canonicalBoundary}`);
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
persona: await loadRequiredMarkdown(canonicalDirectory, profilePersonaFile),
|
|
130
|
+
task: await loadRequiredMarkdown(canonicalDirectory, profileTaskFile),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async function loadPersonaAndTask(profileId, workspace, profileDirectory) {
|
|
134
|
+
const workspaceDirectory = workspaceProfileDirectory(workspace);
|
|
135
|
+
if (await markdownPairExists(workspaceDirectory)) {
|
|
136
|
+
return loadMarkdownPair(workspaceDirectory, workspace);
|
|
137
|
+
}
|
|
138
|
+
if (await markdownPairExists(profileDirectory)) {
|
|
139
|
+
return loadMarkdownPair(profileDirectory, profileDirectory);
|
|
140
|
+
}
|
|
141
|
+
const missing = [
|
|
142
|
+
...(await missingMarkdownPaths(workspaceDirectory)),
|
|
143
|
+
...(await missingMarkdownPaths(profileDirectory)),
|
|
144
|
+
];
|
|
145
|
+
throw new Error(`Profile ${profileId} has no complete ${profilePersonaFile} + ${profileTaskFile} pair. Missing: ${missing.join(", ")}`);
|
|
146
|
+
}
|
|
84
147
|
export async function loadProfile(profileId, profilesDirectory = join(homedir(), ".beacon", "profiles")) {
|
|
85
148
|
assertProfileId(profileId);
|
|
86
149
|
const profileDirectory = join(profilesDirectory, profileId);
|
|
@@ -105,8 +168,6 @@ export async function loadProfile(profileId, profilesDirectory = join(homedir(),
|
|
|
105
168
|
throw new Error(`Profile ${profileId} with schedules must declare admin.chat_id`);
|
|
106
169
|
}
|
|
107
170
|
const canonicalProfileDirectory = await realpath(profileDirectory);
|
|
108
|
-
const persona = await loadRequiredMarkdown(canonicalProfileDirectory, profilePersonaFile);
|
|
109
|
-
const task = await loadRequiredMarkdown(canonicalProfileDirectory, profileTaskFile);
|
|
110
171
|
const workspace = isAbsolute(config.workspace)
|
|
111
172
|
? config.workspace
|
|
112
173
|
: resolve(dirname(configPath), config.workspace);
|
|
@@ -122,6 +183,7 @@ export async function loadProfile(profileId, profilesDirectory = join(homedir(),
|
|
|
122
183
|
if (!workspaceStat.isDirectory()) {
|
|
123
184
|
throw new Error(`Profile workspace is not a directory: ${workspace}`);
|
|
124
185
|
}
|
|
186
|
+
const { persona, task } = await loadPersonaAndTask(profileId, workspace, canonicalProfileDirectory);
|
|
125
187
|
return {
|
|
126
188
|
id: profileId,
|
|
127
189
|
directory: canonicalProfileDirectory,
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|