@hachej/boring-agent 0.4.24 → 0.4.25
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 +6 -6
- package/dist/delegate.d.ts +2 -1
- package/dist/delegate.js +10 -4
- package/dist/skills.d.ts +11 -5
- package/dist/skills.js +36 -7
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -367,10 +367,10 @@ Pi renders every extension section before each model request (`PromptSection.ren
|
|
|
367
367
|
**Control (all optional; with none set, the extension behaves as above).**
|
|
368
368
|
|
|
369
369
|
- `visible(conversationId, skills)`: which skills a conversation sees. It is called once per render and once per `load_skill` call (may be async) with the whole current list of valid skills, and returns the subset that conversation sees (matched by name). The id is Pi's own: the section gets it from `PromptInput.conversationId`, the tool from `ToolExecutionApi.conversationId`, so no host bookkeeping is needed. A hidden skill is left out of the listing, and `load_skill` answers for it exactly as for an unknown name (`Unknown skill "<name>". Available: <visible names>`). With a filter, a skill that could not be read also answers as unknown (it is never in the list); its reason still reaches `onDiagnostics`. Works for an array too.
|
|
370
|
-
- `companionFiles: true`: `load_skill`'s answer ends with the files beside the skill's `SKILL.md` (the Agent Skills layout: `references/`, `scripts/`, `assets/`; any depth, name order, at most 200), and `load_skill({ name, path })` returns one of them as text. The files are checked when the listing is read,
|
|
370
|
+
- `companionFiles: true`: `load_skill`'s answer ends with the files beside the skill's `SKILL.md` (the Agent Skills layout: `references/`, `scripts/`, `assets/`; any depth, name order, at most 200), and `load_skill({ name, path })` returns one of them as text. The files are checked when the listing is read, and a listed file is checked again when the model asks: each one must be a regular file inside the skill's folder that reads as UTF-8 text. A symbolic link, an unreadable or binary file (a NUL byte or invalid UTF-8) and files past the 200th are left out and reported to `onDiagnostics` (`{ path, name: <skill>, message: 'Companion file left out: …' }`). Only a listed path is read, so a path cannot leave the folder (`../`, absolute paths and links are refused with `Skill "<name>" has no companion file "<path>"`). Before returning a listed file, `load_skill` looks at the skill's folder, every folder on the way and the file with `fileInfo` (which does not follow links) and checks the text again: a file or folder swapped for a symbolic link since the listing (the agent may write its own skill folder) answers `Companion file "<path>" of skill "<name>" could not be read: <path> is a symbolic link` and is never followed. The list is kept with the listing: a companion added or removed later shows up on the next read of the source (a `SKILL.md` change or `refresh()`); a listed file's content is read on demand through the source's `fs` (the same workspace `FileSystem`; both built-in sources set `SkillSource.fs`), never cached. A skill file not named `SKILL.md` (a `fileSkillSource` on `notes/triage.md`) has none. An array, or a source without `fs`, throws.
|
|
371
371
|
- `intro`: the listing section's text before the `- name: description` lines, replacing the default (“Before starting a task that matches a skill, call load_skill …”, with the `/<skill-name>` rule). The skill lines still follow it.
|
|
372
|
-
- `onPublish(next, previous)`: called with both listings' skills before a newly read listing of a source replaces the current one (may be async). Return a reason, or throw, to refuse it: the current listing stays and `onDiagnostics` gets `
|
|
373
|
-
- `createSkillsController(name, source, options?)` returns `{ extension, refresh }`, so `createSkillsExtension` keeps its return type (it is `createSkillsController(...).extension` for a source). `await refresh(context)` reads the source now, whatever `maxAgeMs`, and publishes it through `onPublish`; `refresh(context, name)` takes only that skill from the new read (added, replaced or removed) and keeps the others as they were. It resolves `true` when published, `false` when refused. Reads run one at a time, so a refresh never interleaves with a `maxAgeMs` check. Call it from the code that changed the skills (an editor save, a self-evolution keep); there is still no watcher.
|
|
372
|
+
- `onPublish(next, previous)`: called with both listings' skills before a newly read listing of a source replaces the current one (may be async). Return a reason, or throw, to refuse it: the current listing stays and `onDiagnostics` gets that listing's errors followed by `{ path: '', message: 'Listing refused by onPublish: <reason>' }` (the per-skill errors are kept; the entry with `path: ''` is the refusal). A refused listing is not offered again until the source's version changes or `refresh()` runs. The first read is offered too (`previous` empty).
|
|
373
|
+
- `createSkillsController(name, source, options?)` returns `{ extension, refresh }`, so `createSkillsExtension` keeps its return type (it is `createSkillsController(...).extension` for a source). `await refresh(context)` reads the source now, whatever `maxAgeMs`, and publishes it through `onPublish`; `refresh(context, name)` takes only that skill from the new read (added, replaced or removed) and keeps the others as they were until the source's `version()` changes again: later `maxAgeMs` checks and `load_skill` misses do not re-read an unchanged source, and the first read after a change offers the whole source (through `onPublish`) as usual. A source only lists everything at once, so the others' pending edits are part of that next read. It resolves `true` when published, `false` when refused. Reads run one at a time, so a refresh never interleaves with a `maxAgeMs` check. Call it from the code that changed the skills (an editor save, a self-evolution keep); there is still no watcher.
|
|
374
374
|
|
|
375
375
|
**Refresh on miss.** On a source, `load_skill` for a name not in the conversation's listing checks the source once before answering, whatever `maxAgeMs`: `version()` now, and `list()` plus `onPublish` only when the version changed, serialized with the other reads. So a skill written a moment ago loads on the first try, and the listing shows it from the next request on. A model guessing names costs one `version()` per miss (a `fileInfo` per skill folder for the built-in sources), never a re-read of an unchanged source, and concurrent misses share one check in flight, so there is no time floor. A miss of a refused listing's skill stays a miss until the version changes or `refresh()` runs, as above.
|
|
376
376
|
|
|
@@ -412,12 +412,12 @@ const delegates = [{ agent: researcher, onChild: async (parent, child, tx) => {
|
|
|
412
412
|
| `limits.maxStartsPerHour` | `number` | Children started for one parent conversation in the last 60 minutes (rolling), ended or not. |
|
|
413
413
|
| `limits.maxTaskChars` | `number` | Characters of `task`. |
|
|
414
414
|
| `limits.timeoutMs` | `number` | Wall-clock milliseconds from the call's first start (kept across a rerun). On expiry the tool aborts the child conversation (`ConversationHandle.abort`, the same cancel path as a stopped call) and the call fails: `details.status` `failed`, `error` `The <agent> agent timed out after <ms> ms.` |
|
|
415
|
-
| `meter` | `(parent, child) => DelegateMeter \| Promise<DelegateMeter>` | The host's handle for the child's spend: the tool calls `record(event)` once per child model response (`DelegateModelEvent`: `entryId`, `provider`, `model`, Pi's per-response `usage`) and `close(end)` once at the end (`done
|
|
415
|
+
| `meter` | `(parent, child) => DelegateMeter \| Promise<DelegateMeter>` | The host's handle for the child's spend: the tool calls `record(event)` once per child model response (`DelegateModelEvent`: `entryId`, `provider`, `model`, Pi's per-response `usage`) and `close(end)` once at the end (`done` once the child's answer is committed to the parent; `cancelled` only when the call or the child was aborted; `timeout`; `failed` for every other end, including a throw such as a failed commit). The kit keeps no log. |
|
|
416
416
|
| `childTools` | `string[]` | Names of the tools a child may have: its enabled tools are the intersection with what it would otherwise get, written as Pi's own `pi.agent` `tools` filter in the creating commit. |
|
|
417
417
|
|
|
418
|
-
A call over a limit is refused: it creates no child and returns an ordinary error result the model reads, for example `2 delegates already running for this conversation (limit 2).`, `1 delegate already running
|
|
418
|
+
A call over a limit is refused: it creates no child and returns an ordinary error result the model reads, for example `2 delegates already running for this conversation (limit 2).`, `1 delegate already running across the conversations sharing this limit (limit 1).`, `1 delegate already started for this conversation in the last hour (limit 1).` or `The task is 41 characters; the limit is 40.` It never throws and carries no `details`. Only a call that would create a child is checked: a rerun that finds its existing child (after a crash or eviction) is never refused, and a call counts once, by its tool task.
|
|
419
419
|
|
|
420
|
-
The counts live in this process and belong to the `limits` object: every delegate tool given the same object shares them, so one object passed to several `delegates` entries is one host-wide cap. Separate processes do not share them, and a restart starts them empty (reruns that reattach count their children again; the hourly window forgets earlier starts). `meter` is called after the commit that creates the child and again by a rerun in a new process that reattaches to it, since the earlier handle is gone; such a rerun sends the child's earlier responses again, so dedupe by `entryId`. A throw from `meter` fails the call; a throw from `record` or `close` is ignored.
|
|
420
|
+
The counts live in this process and belong to the `limits` object: every delegate tool given the same object shares them, so one object passed to several `delegates` entries is one host-wide cap, while two entries written with their own literals (`limits: { maxRunning: 2 }` each) get two separate budgets; the refusal text says “sharing this limit” for that reason. Separate processes do not share them, and a restart starts them empty (reruns that reattach count their children again; the hourly window forgets earlier starts). `meter` is called after the commit that creates the child and again by a rerun in a new process that reattaches to it, since the earlier handle is gone; such a rerun sends the child's earlier responses again, so dedupe by `entryId`. A throw from `meter` fails the call; a throw from `record` or `close` is ignored.
|
|
421
421
|
|
|
422
422
|
```ts
|
|
423
423
|
const limits = { maxRunningPerConversation: 2, maxRunning: 8, maxStartsPerHour: 20, maxTaskChars: 4000, timeoutMs: 300_000 };
|
package/dist/delegate.d.ts
CHANGED
|
@@ -117,7 +117,8 @@ export interface DelegateOptions {
|
|
|
117
117
|
* child's final text. The child inherits the parent's approval and sandbox extensions (appended after its own, in the parent's order, so a
|
|
118
118
|
* sandbox stays last), so a delegate cannot escape the parent's gate; `details` is a `DelegateDetails` (live while the child runs) for a UI that shows it.
|
|
119
119
|
* `onChild` lets the host write the parent's identity onto the child in the same commit that creates it.
|
|
120
|
-
* Host policy, all optional (Pi has no extension point for these): `limits` (running children per conversation and
|
|
120
|
+
* Host policy, all optional (Pi has no extension point for these): `limits` (running children per conversation and across
|
|
121
|
+
* the conversations sharing one `limits` object in this process,
|
|
121
122
|
* starts per hour, task size, a child deadline; a refusal is an error result, never a throw, and creates no child), `meter`
|
|
122
123
|
* (the host's flight-log handle, fed each child model response and closed when the call ends) and `childTools` (an allow list
|
|
123
124
|
* applied through Pi's own tool filter). With none of them set the tool behaves as before.
|
package/dist/delegate.js
CHANGED
|
@@ -13,7 +13,8 @@ export const DELEGATE_DETAILS_KIND = 'boring.delegate';
|
|
|
13
13
|
* child's final text. The child inherits the parent's approval and sandbox extensions (appended after its own, in the parent's order, so a
|
|
14
14
|
* sandbox stays last), so a delegate cannot escape the parent's gate; `details` is a `DelegateDetails` (live while the child runs) for a UI that shows it.
|
|
15
15
|
* `onChild` lets the host write the parent's identity onto the child in the same commit that creates it.
|
|
16
|
-
* Host policy, all optional (Pi has no extension point for these): `limits` (running children per conversation and
|
|
16
|
+
* Host policy, all optional (Pi has no extension point for these): `limits` (running children per conversation and across
|
|
17
|
+
* the conversations sharing one `limits` object in this process,
|
|
17
18
|
* starts per hour, task size, a child deadline; a refusal is an error result, never a throw, and creates no child), `meter`
|
|
18
19
|
* (the host's flight-log handle, fed each child model response and closed when the call ends) and `childTools` (an allow list
|
|
19
20
|
* applied through Pi's own tool filter). With none of them set the tool behaves as before.
|
|
@@ -60,7 +61,8 @@ export function createDelegateTool(options) {
|
|
|
60
61
|
if ('refused' in opened)
|
|
61
62
|
return { output: [{ type: 'text', text: opened.refused }], isError: true };
|
|
62
63
|
const childId = opened.id;
|
|
63
|
-
|
|
64
|
+
// `end` is set where the call's outcome is known; a throw ends `cancelled` only when the call itself was aborted.
|
|
65
|
+
let meter, end = 'failed', timer;
|
|
64
66
|
try {
|
|
65
67
|
meter = await options.meter?.(parent, childId);
|
|
66
68
|
const startedAt = await api.memo('delegate.startedAt', Date.now(), context);
|
|
@@ -96,12 +98,16 @@ export function createDelegateTool(options) {
|
|
|
96
98
|
end = reason === 'aborted' ? 'cancelled' : 'failed';
|
|
97
99
|
return { output: [{ type: 'text', text: error }], isError: true, details: reason === 'aborted' ? details('cancelled', counts) : details('failed', counts, error) };
|
|
98
100
|
}
|
|
99
|
-
end = 'done';
|
|
100
101
|
const answer = await api.commit(tx => tx.entry(AssistantEntry, settled.answer), context);
|
|
102
|
+
end = 'done';
|
|
101
103
|
const message = answer?.model?.find(part => part.role === 'assistant');
|
|
102
104
|
const text = message?.role === 'assistant' ? message.content.flatMap(part => part.type === 'text' ? [part.text] : []).join('\n') : '';
|
|
103
105
|
return { output: [{ type: 'text', text }], details: details('done', counts) };
|
|
104
106
|
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
end = context.abortSignal?.aborted ? 'cancelled' : 'failed';
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
105
111
|
finally {
|
|
106
112
|
clearTimeout(timer);
|
|
107
113
|
ledger?.running.delete(api.taskId);
|
|
@@ -216,7 +222,7 @@ function admit(limits, ledger, taskId, parent, task) {
|
|
|
216
222
|
if (limits.maxRunningPerConversation !== undefined && here >= limits.maxRunningPerConversation)
|
|
217
223
|
return `${delegates(here)} already running for this conversation (limit ${limits.maxRunningPerConversation}).`;
|
|
218
224
|
if (limits.maxRunning !== undefined && others.length >= limits.maxRunning)
|
|
219
|
-
return `${delegates(others.length)} already running
|
|
225
|
+
return `${delegates(others.length)} already running across the conversations sharing this limit (limit ${limits.maxRunning}).`;
|
|
220
226
|
const started = [...ledger.starts].filter(([id, start]) => id !== taskId && start.parent === parent).length;
|
|
221
227
|
if (limits.maxStartsPerHour !== undefined && started >= limits.maxStartsPerHour)
|
|
222
228
|
return `${delegates(started)} already started for this conversation in the last hour (limit ${limits.maxStartsPerHour}).`;
|
package/dist/skills.d.ts
CHANGED
|
@@ -43,7 +43,10 @@ export declare function fileSkillSource(fs: SkillFileSystem, path: string): Skil
|
|
|
43
43
|
export interface SkillsExtensionOptions {
|
|
44
44
|
/** How long a source's copy is used before `version()` is asked again, in milliseconds. Default 5000. Unused for an array. */
|
|
45
45
|
readonly maxAgeMs?: number;
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Called after each read of a source with every skill it left out (an empty array when none). When `onPublish` refuses the
|
|
48
|
+
* read, it gets the errors of the listing still in effect followed by `{ path: '', message: 'Listing refused by onPublish: <reason>' }`.
|
|
49
|
+
*/
|
|
47
50
|
readonly onDiagnostics?: (errors: readonly SkillError[]) => void;
|
|
48
51
|
/**
|
|
49
52
|
* Which skills a conversation sees: called once per listing render and per `load_skill` call with Pi's conversation id and
|
|
@@ -55,7 +58,9 @@ export interface SkillsExtensionOptions {
|
|
|
55
58
|
/**
|
|
56
59
|
* List the files beside each `SKILL.md` (at any depth, at most 200) in `load_skill`'s answer, and let
|
|
57
60
|
* `load_skill({ name, path })` return one of them as text. Each file is checked when the listing is read: a symbolic link,
|
|
58
|
-
* a file that cannot be read or one that is not UTF-8 text is left out and reported to `onDiagnostics`.
|
|
61
|
+
* a file that cannot be read or one that is not UTF-8 text is left out and reported to `onDiagnostics`. `load_skill` checks a
|
|
62
|
+
* listed file again before returning it (the skill's folder, every folder on the way and the file itself, with `fileInfo`,
|
|
63
|
+
* which does not follow links), so a file or folder swapped for a link since the listing is refused. Read through the
|
|
59
64
|
* source's `fs`; an array or a source without `fs` throws.
|
|
60
65
|
*/
|
|
61
66
|
readonly companionFiles?: boolean;
|
|
@@ -63,7 +68,7 @@ export interface SkillsExtensionOptions {
|
|
|
63
68
|
readonly intro?: string;
|
|
64
69
|
/**
|
|
65
70
|
* Called before a newly read listing of a source replaces the current one, with both listings' skills. Return a reason
|
|
66
|
-
* (or throw) to refuse it: the current listing stays, and `onDiagnostics` gets `Listing refused by onPublish: <reason>`.
|
|
71
|
+
* (or throw) to refuse it: the current listing stays, and `onDiagnostics` gets its errors plus `Listing refused by onPublish: <reason>`.
|
|
67
72
|
* A refused listing is offered again only once the source's version changes or `refresh()` is called.
|
|
68
73
|
*/
|
|
69
74
|
readonly onPublish?: (next: readonly Skill[], previous: readonly Skill[]) => string | void | Promise<string | void>;
|
|
@@ -73,8 +78,9 @@ export interface SkillsController {
|
|
|
73
78
|
readonly extension: Extension;
|
|
74
79
|
/**
|
|
75
80
|
* Read the source now, whatever `maxAgeMs`, and publish what it holds (through `onPublish`). With `name`, only that skill
|
|
76
|
-
* is taken from the new read (added, replaced or removed); the others stay as they were
|
|
77
|
-
* `
|
|
81
|
+
* is taken from the new read (added, replaced or removed); the others stay as they were until the source's `version()`
|
|
82
|
+
* changes again, when the next read offers the whole source as usual. Resolves `true` when published, `false` when
|
|
83
|
+
* `onPublish` refused it.
|
|
78
84
|
*/
|
|
79
85
|
readonly refresh: (context: Context, name?: string) => Promise<boolean>;
|
|
80
86
|
}
|
package/dist/skills.js
CHANGED
|
@@ -163,8 +163,9 @@ function liveSkills(source, options) {
|
|
|
163
163
|
}
|
|
164
164
|
async function publish(next) {
|
|
165
165
|
const refused = await refusal(next);
|
|
166
|
+
// The listing in effect stays, so its errors stay too: the refusal is added to them, never replaces them.
|
|
166
167
|
if (refused !== undefined) {
|
|
167
|
-
options.onDiagnostics?.([{ path: '', message: `Listing refused by onPublish: ${refused}` }]);
|
|
168
|
+
options.onDiagnostics?.([...state.errors, { path: '', message: `Listing refused by onPublish: ${refused}` }]);
|
|
168
169
|
return false;
|
|
169
170
|
}
|
|
170
171
|
state = next;
|
|
@@ -192,8 +193,9 @@ function liveSkills(source, options) {
|
|
|
192
193
|
miss: (context) => missing ??= serial(() => check(context)).finally(() => { missing = undefined; }),
|
|
193
194
|
refresh: (context, name) => serial(async () => {
|
|
194
195
|
const next = await source.version(context), listing = await read(context);
|
|
195
|
-
//
|
|
196
|
-
|
|
196
|
+
// The read is accounted for at this version, whether all of it or one skill was taken (or onPublish refused it): the
|
|
197
|
+
// others kept by a one-skill refresh are offered again only once the source changes, not at the next maxAgeMs check.
|
|
198
|
+
version = next;
|
|
197
199
|
checkedAt = Date.now();
|
|
198
200
|
return publish(name === undefined ? listing : only(name, listing, state));
|
|
199
201
|
}),
|
|
@@ -203,6 +205,26 @@ const INTRO_TEXT = 'Before starting a task that matches a skill, call load_skill
|
|
|
203
205
|
const COMPANION_LIMIT = 200;
|
|
204
206
|
/** Each skill's companion files, checked when its listing was read. */
|
|
205
207
|
const companionLists = new WeakMap();
|
|
208
|
+
const notText = (text) => /[\u0000\uFFFD]/.test(text);
|
|
209
|
+
/**
|
|
210
|
+
* Why a listed companion may not be read now, or `undefined`: the skill's folder and every folder on the way must still be
|
|
211
|
+
* directories and the file a regular file (`fileInfo` does not follow links), so a link swapped in since the listing is refused.
|
|
212
|
+
*/
|
|
213
|
+
async function companionChanged(fs, folder, relative, context) {
|
|
214
|
+
const parts = relative.split('/'), base = folder === '.' ? '' : `${folder}/`;
|
|
215
|
+
const steps = [...(folder === '.' ? [] : [{ path: folder, kind: 'directory' }]),
|
|
216
|
+
...parts.map((_, index) => ({ path: base + parts.slice(0, index + 1).join('/'), kind: index === parts.length - 1 ? 'file' : 'directory' }))];
|
|
217
|
+
for (const step of steps) {
|
|
218
|
+
const info = await fs.fileInfo(step.path, context);
|
|
219
|
+
if (!info.ok)
|
|
220
|
+
return info.error.message;
|
|
221
|
+
if (info.value.kind === 'symlink')
|
|
222
|
+
return `${step.path} is a symbolic link`;
|
|
223
|
+
if (info.value.kind !== step.kind)
|
|
224
|
+
return `${step.path} is not a ${step.kind === 'file' ? 'regular file' : 'directory'}`;
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
206
228
|
/**
|
|
207
229
|
* Walk the files beside each skill's `SKILL.md` (relative to its folder, depth first in name order, at most
|
|
208
230
|
* `COMPANION_LIMIT`) and keep those that read as UTF-8 text. A symbolic link, an unreadable or non-text file, and files past
|
|
@@ -246,7 +268,7 @@ async function withCompanions(fs, listing, context) {
|
|
|
246
268
|
const text = await fs.readTextFile(at(path), context);
|
|
247
269
|
if (!text.ok)
|
|
248
270
|
leftOut(path, `Companion file left out: ${text.error.message}`);
|
|
249
|
-
else if (
|
|
271
|
+
else if (notText(text.value))
|
|
250
272
|
leftOut(path, 'Companion file left out: not UTF-8 text');
|
|
251
273
|
else
|
|
252
274
|
files.push(path);
|
|
@@ -298,12 +320,19 @@ function skillsExtension(name, live, options, fs) {
|
|
|
298
320
|
const folder = skillFolders.get(skill);
|
|
299
321
|
const companions = fs ? companionLists.get(skill) ?? [] : [];
|
|
300
322
|
if (fs && args.path !== undefined) {
|
|
301
|
-
// Only a listed file is read, so the path cannot leave the folder
|
|
323
|
+
// Only a listed file is read, so the path cannot leave the folder; it is checked again now, so a link swapped in
|
|
324
|
+
// since the listing (the agent may write its own skill folder) is refused rather than followed.
|
|
302
325
|
const path = args.path.replace(/^(\.\/)+/, '');
|
|
303
|
-
if (!companions.includes(path))
|
|
326
|
+
if (!companions.includes(path) || folder === undefined)
|
|
304
327
|
return text(`Skill "${skill.name}" has no companion file "${args.path}". Companion files: ${companions.join(', ') || 'none'}`);
|
|
328
|
+
const failed = (why) => text(`Companion file "${path}" of skill "${skill.name}" could not be read: ${why}`);
|
|
329
|
+
const changed = await companionChanged(fs, folder, path, context);
|
|
330
|
+
if (changed !== undefined)
|
|
331
|
+
return failed(changed);
|
|
305
332
|
const read = await fs.readTextFile(folder === '.' ? path : `${folder}/${path}`, context);
|
|
306
|
-
|
|
333
|
+
if (!read.ok)
|
|
334
|
+
return failed(read.error.message);
|
|
335
|
+
return notText(read.value) ? failed('not UTF-8 text') : text(read.value);
|
|
307
336
|
}
|
|
308
337
|
const location = folder === undefined ? '' : `\n\nThis skill's folder is \`${folder}\`; paths in it are relative to that folder.`;
|
|
309
338
|
const listed = companions.length ? `\n\nCompanion files of this skill (read one with load_skill and its path):\n${companions.map(path => `- ${path}`).join('\n')}` : '';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-agent",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.25",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -160,8 +160,8 @@
|
|
|
160
160
|
"@earendil-works/pi-durable": "~1.2.0",
|
|
161
161
|
"@earendil-works/pi-ai": "~1.2.0",
|
|
162
162
|
"@earendil-works/chord": "~1.2.0",
|
|
163
|
-
"@hachej/boring-files": "^0.4.
|
|
164
|
-
"@hachej/boring-ui-kit": "^0.4.
|
|
163
|
+
"@hachej/boring-files": "^0.4.25",
|
|
164
|
+
"@hachej/boring-ui-kit": "^0.4.25",
|
|
165
165
|
"xlsx": "^0.20.3"
|
|
166
166
|
},
|
|
167
167
|
"peerDependenciesMeta": {
|