pi-gentic 0.1.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 +499 -0
- package/dist/commands.d.ts +93 -0
- package/dist/commands.js +422 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.js +407 -0
- package/dist/core.d.ts +19 -0
- package/dist/core.js +125 -0
- package/dist/extension.d.ts +2 -0
- package/dist/extension.js +476 -0
- package/dist/orchestrator.d.ts +277 -0
- package/dist/orchestrator.js +633 -0
- package/dist/policy.d.ts +43 -0
- package/dist/policy.js +136 -0
- package/dist/prompt.d.ts +12 -0
- package/dist/prompt.js +226 -0
- package/dist/runs.d.ts +99 -0
- package/dist/runs.js +540 -0
- package/dist/runtime.d.ts +127 -0
- package/dist/runtime.js +360 -0
- package/dist/sessions.d.ts +56 -0
- package/dist/sessions.js +487 -0
- package/dist/ui.d.ts +108 -0
- package/dist/ui.js +957 -0
- package/dist/worktrees.d.ts +1 -0
- package/dist/worktrees.js +86 -0
- package/docs/assets/error-card.png +0 -0
- package/docs/assets/load-agent.png +0 -0
- package/docs/assets/orchestration-tree.png +0 -0
- package/docs/assets/send-background.png +0 -0
- package/docs/assets/send-foreground.png +0 -0
- package/package.json +58 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration loading for pi-gentic.
|
|
3
|
+
*
|
|
4
|
+
* Global and project roots are read in order, with later roots replacing
|
|
5
|
+
* earlier agent definitions. Markdown agents and inline settings end up in the
|
|
6
|
+
* same normalized shape before policy resolution sees them.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { isRecord, toStringArray } from "./core.js";
|
|
12
|
+
const EXTENSION_DIR = path.join("extensions", "pi-gentic");
|
|
13
|
+
export function defaultAgentDir() {
|
|
14
|
+
return (process.env.PI_CODING_AGENT_DIR || path.join(homedir(), ".pi", "agent"));
|
|
15
|
+
}
|
|
16
|
+
export function getConfigRoots(cwd = process.cwd(), agentDir = defaultAgentDir()) {
|
|
17
|
+
const roots = [path.join(agentDir, EXTENSION_DIR)];
|
|
18
|
+
const projectRoot = findNearestProjectConfigRoot(cwd);
|
|
19
|
+
if (projectRoot)
|
|
20
|
+
roots.push(projectRoot);
|
|
21
|
+
return roots;
|
|
22
|
+
}
|
|
23
|
+
function findNearestProjectConfigRoot(cwd) {
|
|
24
|
+
let current = path.resolve(cwd);
|
|
25
|
+
while (true) {
|
|
26
|
+
const candidate = path.join(current, ".pi", EXTENSION_DIR);
|
|
27
|
+
if (existsSync(candidate))
|
|
28
|
+
return candidate;
|
|
29
|
+
const parent = path.dirname(current);
|
|
30
|
+
if (parent === current)
|
|
31
|
+
return null;
|
|
32
|
+
current = parent;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Loads settings, agents, diagnostics, and config roots for one working directory. */
|
|
36
|
+
export function loadPiSettings(agentDir = defaultAgentDir()) {
|
|
37
|
+
return readJson(path.join(agentDir, "settings.json"), []) ?? {};
|
|
38
|
+
}
|
|
39
|
+
export function enabledModelPatterns(agentDir = defaultAgentDir()) {
|
|
40
|
+
const settings = loadPiSettings(agentDir);
|
|
41
|
+
return toStringArray(settings.enabledModels);
|
|
42
|
+
}
|
|
43
|
+
export function loadConfiguration(options = {}) {
|
|
44
|
+
const cwd = typeof options.cwd === "string" ? options.cwd : undefined;
|
|
45
|
+
const agentDir = typeof options.agentDir === "string" ? options.agentDir : undefined;
|
|
46
|
+
const roots = Array.isArray(options.roots)
|
|
47
|
+
? options.roots.filter((root) => typeof root === "string")
|
|
48
|
+
: getConfigRoots(cwd, agentDir);
|
|
49
|
+
const settings = createDefaultSettings();
|
|
50
|
+
const agentsByName = new Map();
|
|
51
|
+
const diagnostics = [];
|
|
52
|
+
for (const root of roots) {
|
|
53
|
+
const settingsPath = path.join(root, "settings.json");
|
|
54
|
+
const rootSettings = readJson(settingsPath, diagnostics);
|
|
55
|
+
if (rootSettings) {
|
|
56
|
+
mergeRootSettings(settings, rootSettings);
|
|
57
|
+
for (const definition of normalizeAgentDefinitions(rootSettings.agentDefinitions, settingsPath, diagnostics)) {
|
|
58
|
+
agentsByName.set(String(definition.name), definition);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const definition of loadMarkdownAgents(path.join(root, "agents"), diagnostics)) {
|
|
62
|
+
agentsByName.set(String(definition.name), definition);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const agents = [...agentsByName.values()].filter((agent) => agent.disabled !== true);
|
|
66
|
+
return {
|
|
67
|
+
settings: { ...settings, agentDefinitions: agents },
|
|
68
|
+
agents,
|
|
69
|
+
diagnostics,
|
|
70
|
+
roots,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function createDefaultSettings() {
|
|
74
|
+
return {
|
|
75
|
+
agentlessSession: {},
|
|
76
|
+
agentDefinitions: [],
|
|
77
|
+
agentDefaults: {},
|
|
78
|
+
globalMaxSubagentDepth: 6,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function readJson(filePath, diagnostics) {
|
|
82
|
+
if (!existsSync(filePath))
|
|
83
|
+
return undefined;
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(readFileSync(filePath, "utf8"));
|
|
86
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
diagnostics.push({
|
|
90
|
+
severity: "error",
|
|
91
|
+
path: filePath,
|
|
92
|
+
message: `Could not parse JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
93
|
+
});
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function mergeRootSettings(target, source) {
|
|
98
|
+
if (!isRecord(source))
|
|
99
|
+
return;
|
|
100
|
+
if (isRecord(source.agentlessSession))
|
|
101
|
+
target.agentlessSession = mergeObjects(target.agentlessSession, source.agentlessSession);
|
|
102
|
+
if (isRecord(source.agentDefaults))
|
|
103
|
+
target.agentDefaults = mergeObjects(target.agentDefaults, source.agentDefaults);
|
|
104
|
+
if (typeof source.defaultAgent === "string" || source.defaultAgent === null)
|
|
105
|
+
target.defaultAgent = source.defaultAgent;
|
|
106
|
+
if (Number.isFinite(Number(source.globalMaxSubagentDepth))) {
|
|
107
|
+
target.globalMaxSubagentDepth = Math.floor(Number(source.globalMaxSubagentDepth));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function mergeObjects(base, patch) {
|
|
111
|
+
const result = { ...base };
|
|
112
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
113
|
+
result[key] =
|
|
114
|
+
isRecord(value) && isRecord(result[key])
|
|
115
|
+
? mergeObjects(result[key], value)
|
|
116
|
+
: value;
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
function loadMarkdownAgents(dir, diagnostics) {
|
|
121
|
+
if (!existsSync(dir))
|
|
122
|
+
return [];
|
|
123
|
+
let entries = [];
|
|
124
|
+
try {
|
|
125
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
diagnostics.push({
|
|
129
|
+
severity: "warning",
|
|
130
|
+
path: dir,
|
|
131
|
+
message: `Could not read agents directory: ${error instanceof Error ? error.message : String(error)}`,
|
|
132
|
+
});
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
return entries
|
|
136
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
|
|
137
|
+
.flatMap((entry) => loadMarkdownAgent(path.join(dir, entry.name), diagnostics));
|
|
138
|
+
}
|
|
139
|
+
function loadMarkdownAgent(filePath, diagnostics) {
|
|
140
|
+
try {
|
|
141
|
+
const content = readFileSync(filePath, "utf8");
|
|
142
|
+
const { frontmatter, body } = parseMarkdownDefinition(content);
|
|
143
|
+
const metadata = frontmatter;
|
|
144
|
+
const definition = normalizeAgentDefinition({ ...metadata, instructions: body.trim() || metadata.instructions }, filePath, diagnostics);
|
|
145
|
+
return definition ? [definition] : [];
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
diagnostics.push({
|
|
149
|
+
severity: "warning",
|
|
150
|
+
path: filePath,
|
|
151
|
+
message: `Could not load agent: ${error instanceof Error ? error.message : String(error)}`,
|
|
152
|
+
});
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function normalizeAgentDefinitions(value, sourcePath, diagnostics) {
|
|
157
|
+
if (!Array.isArray(value))
|
|
158
|
+
return [];
|
|
159
|
+
return value.flatMap((item, index) => {
|
|
160
|
+
const definition = normalizeAgentDefinition(item, `${sourcePath}#agentDefinitions[${index}]`, diagnostics);
|
|
161
|
+
return definition ? [definition] : [];
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/** Turns user-authored JSON or frontmatter into the canonical agent shape. */
|
|
165
|
+
export function normalizeAgentDefinition(value, sourcePath = "inline", diagnostics = []) {
|
|
166
|
+
if (!isRecord(value))
|
|
167
|
+
return undefined;
|
|
168
|
+
const name = typeof value.name === "string" ? value.name.trim() : "";
|
|
169
|
+
if (!name) {
|
|
170
|
+
diagnostics.push({
|
|
171
|
+
severity: "warning",
|
|
172
|
+
path: sourcePath,
|
|
173
|
+
message: "Ignored unnamed agent definition.",
|
|
174
|
+
});
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
const model = typeof value.model === "string"
|
|
178
|
+
? value.model
|
|
179
|
+
: Array.isArray(value.models)
|
|
180
|
+
? value.models.find((item) => typeof item === "string")
|
|
181
|
+
: undefined;
|
|
182
|
+
return removeUndefined({
|
|
183
|
+
name,
|
|
184
|
+
description: typeof value.description === "string" ? value.description : "",
|
|
185
|
+
instructions: typeof value.instructions === "string" ? value.instructions : "",
|
|
186
|
+
disabled: value.disabled === true,
|
|
187
|
+
agents: toStringArray(value.agents),
|
|
188
|
+
tools: toStringArray(value.tools),
|
|
189
|
+
skills: toStringArray(value.skills),
|
|
190
|
+
model,
|
|
191
|
+
thinking: typeof value.thinking === "string" ? value.thinking : undefined,
|
|
192
|
+
theme: typeof value.theme === "string" ? value.theme : undefined,
|
|
193
|
+
systemPromptFiles: toStringArray(value.systemPromptFiles),
|
|
194
|
+
maxSubagentDepth: numberOrUndefined(value.maxSubagentDepth),
|
|
195
|
+
agentsTool: normalizeAgentsTool(value.agentsTool),
|
|
196
|
+
sourcePath,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function normalizeAgentsTool(value) {
|
|
200
|
+
if (!isRecord(value))
|
|
201
|
+
return undefined;
|
|
202
|
+
return removeUndefined({
|
|
203
|
+
async: booleanOrUndefined(value.async),
|
|
204
|
+
fork: booleanOrUndefined(value.fork),
|
|
205
|
+
cwd: typeof value.cwd === "string" ? value.cwd : undefined,
|
|
206
|
+
invokeMeLater: isRecord(value.invokeMeLater)
|
|
207
|
+
? removeUndefined({
|
|
208
|
+
async: booleanOrUndefined(value.invokeMeLater.async),
|
|
209
|
+
withSession: booleanOrUndefined(value.invokeMeLater.withSession),
|
|
210
|
+
})
|
|
211
|
+
: undefined,
|
|
212
|
+
rx: numberOrUndefined(value.rx),
|
|
213
|
+
ry: numberOrUndefined(value.ry),
|
|
214
|
+
open: booleanOrUndefined(value.open),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
function booleanOrUndefined(value) {
|
|
218
|
+
return typeof value === "boolean" ? value : undefined;
|
|
219
|
+
}
|
|
220
|
+
function numberOrUndefined(value) {
|
|
221
|
+
const number = Number(value);
|
|
222
|
+
return Number.isFinite(number) ? Math.floor(number) : undefined;
|
|
223
|
+
}
|
|
224
|
+
function removeUndefined(object) {
|
|
225
|
+
return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== undefined));
|
|
226
|
+
}
|
|
227
|
+
/** Parses the small frontmatter subset used by agent and skill markdown files. */
|
|
228
|
+
export function parseMarkdownDefinition(content) {
|
|
229
|
+
if (!content.startsWith("---"))
|
|
230
|
+
return { frontmatter: {}, body: content };
|
|
231
|
+
const end = content.indexOf("\n---", 3);
|
|
232
|
+
if (end === -1)
|
|
233
|
+
return { frontmatter: {}, body: content };
|
|
234
|
+
const yaml = content.slice(3, end).trim();
|
|
235
|
+
const body = content.slice(end + 4).replace(/^\r?\n/, "");
|
|
236
|
+
return { frontmatter: parseSimpleYaml(yaml), body };
|
|
237
|
+
}
|
|
238
|
+
function parseSimpleYaml(yaml) {
|
|
239
|
+
const root = {};
|
|
240
|
+
const lines = yaml.split(/\r?\n/);
|
|
241
|
+
let currentKey;
|
|
242
|
+
let currentNestedKey;
|
|
243
|
+
for (const line of lines) {
|
|
244
|
+
if (!line.trim() || line.trim().startsWith("#"))
|
|
245
|
+
continue;
|
|
246
|
+
const listMatch = line.match(/^\s*-\s*(.*)$/);
|
|
247
|
+
if (listMatch && currentKey) {
|
|
248
|
+
if (currentNestedKey) {
|
|
249
|
+
const target = ensureArraySlot(ensureRecordSlot(root, currentKey), currentNestedKey);
|
|
250
|
+
target.push(parseScalar(listMatch[1]));
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
ensureArraySlot(root, currentKey).push(parseScalar(listMatch[1]));
|
|
254
|
+
}
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const nestedMatch = line.match(/^\s{2,}([A-Za-z][\w.-]*):\s*(.*)$/);
|
|
258
|
+
if (nestedMatch && currentKey && isRecord(root[currentKey])) {
|
|
259
|
+
currentNestedKey = nestedMatch[1];
|
|
260
|
+
const current = ensureRecordSlot(root, currentKey);
|
|
261
|
+
current[currentNestedKey] = nestedMatch[2]
|
|
262
|
+
? parseScalar(nestedMatch[2])
|
|
263
|
+
: [];
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const keyMatch = line.match(/^([A-Za-z][\w.-]*):\s*(.*)$/);
|
|
267
|
+
if (!keyMatch)
|
|
268
|
+
continue;
|
|
269
|
+
currentKey = keyMatch[1];
|
|
270
|
+
currentNestedKey = undefined;
|
|
271
|
+
root[currentKey] = keyMatch[2] ? parseScalar(keyMatch[2]) : [];
|
|
272
|
+
if (currentKey.includes(".")) {
|
|
273
|
+
assignDotted(root, currentKey, root[currentKey]);
|
|
274
|
+
delete root[currentKey];
|
|
275
|
+
currentKey = currentKey.split(".")[0];
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return root;
|
|
279
|
+
}
|
|
280
|
+
function ensureRecordSlot(root, key) {
|
|
281
|
+
if (!isRecord(root[key]))
|
|
282
|
+
root[key] = {};
|
|
283
|
+
return root[key];
|
|
284
|
+
}
|
|
285
|
+
function ensureArraySlot(root, key) {
|
|
286
|
+
if (!Array.isArray(root[key]))
|
|
287
|
+
root[key] = [];
|
|
288
|
+
return root[key];
|
|
289
|
+
}
|
|
290
|
+
function assignDotted(root, key, value) {
|
|
291
|
+
const parts = key.split(".");
|
|
292
|
+
let current = root;
|
|
293
|
+
for (const part of parts.slice(0, -1)) {
|
|
294
|
+
current = ensureRecordSlot(current, part);
|
|
295
|
+
}
|
|
296
|
+
current[parts[parts.length - 1]] = value;
|
|
297
|
+
}
|
|
298
|
+
function parseScalar(value) {
|
|
299
|
+
const trimmed = value.trim();
|
|
300
|
+
if (trimmed === "true")
|
|
301
|
+
return true;
|
|
302
|
+
if (trimmed === "false")
|
|
303
|
+
return false;
|
|
304
|
+
if (/^-?\d+(?:\.\d+)?$/.test(trimmed))
|
|
305
|
+
return Number(trimmed);
|
|
306
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
307
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
308
|
+
return trimmed.slice(1, -1);
|
|
309
|
+
}
|
|
310
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
311
|
+
return trimmed
|
|
312
|
+
.slice(1, -1)
|
|
313
|
+
.split(",")
|
|
314
|
+
.map((item) => parseScalar(item))
|
|
315
|
+
.filter((item) => item !== "");
|
|
316
|
+
}
|
|
317
|
+
return trimmed;
|
|
318
|
+
}
|
|
319
|
+
const skillCatalogCache = new Map();
|
|
320
|
+
/** Discovers skill metadata without loading the full skill instructions. */
|
|
321
|
+
export function loadAvailableSkills(options = {}) {
|
|
322
|
+
const cwd = path.resolve(typeof options.cwd === "string" ? options.cwd : process.cwd());
|
|
323
|
+
const agentDir = path.resolve(typeof options.agentDir === "string" ? options.agentDir : defaultAgentDir());
|
|
324
|
+
const configuredSkillRoots = Array.isArray(options.skillRoots)
|
|
325
|
+
? options.skillRoots.filter((root) => typeof root === "string")
|
|
326
|
+
: undefined;
|
|
327
|
+
const roots = configuredSkillRoots?.map((root) => path.resolve(root)) ??
|
|
328
|
+
skillRoots(cwd, agentDir);
|
|
329
|
+
const cacheKey = configuredSkillRoots ? undefined : `${cwd}::${agentDir}`;
|
|
330
|
+
if (cacheKey && skillCatalogCache.has(cacheKey))
|
|
331
|
+
return skillCatalogCache.get(cacheKey);
|
|
332
|
+
const skills = [
|
|
333
|
+
...new Map(roots
|
|
334
|
+
.flatMap(collectMarkdownFiles)
|
|
335
|
+
.map(loadSkillEntry)
|
|
336
|
+
.filter(Boolean)
|
|
337
|
+
.map((entry) => [entry.name, entry])).values(),
|
|
338
|
+
];
|
|
339
|
+
if (cacheKey)
|
|
340
|
+
skillCatalogCache.set(cacheKey, skills);
|
|
341
|
+
return skills;
|
|
342
|
+
}
|
|
343
|
+
function skillRoots(cwd, agentDir) {
|
|
344
|
+
return dedupePaths([
|
|
345
|
+
path.join(agentDir, "skills"),
|
|
346
|
+
path.join(homedir(), ".agents", "skills"),
|
|
347
|
+
...ancestorDirs(cwd, ".agents", "skills"),
|
|
348
|
+
...ancestorDirs(cwd, ".pi", "skills"),
|
|
349
|
+
]);
|
|
350
|
+
}
|
|
351
|
+
function ancestorDirs(cwd, ...parts) {
|
|
352
|
+
const dirs = [];
|
|
353
|
+
for (let current = path.resolve(cwd);; current = path.dirname(current)) {
|
|
354
|
+
dirs.unshift(path.join(current, ...parts));
|
|
355
|
+
if (path.dirname(current) === current)
|
|
356
|
+
return dirs;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function dedupePaths(paths) {
|
|
360
|
+
return [...new Set(paths.map((item) => path.resolve(item)))];
|
|
361
|
+
}
|
|
362
|
+
function collectMarkdownFiles(dir) {
|
|
363
|
+
if (!existsSync(dir))
|
|
364
|
+
return [];
|
|
365
|
+
let entries = [];
|
|
366
|
+
try {
|
|
367
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
const files = [];
|
|
373
|
+
for (const entry of entries) {
|
|
374
|
+
if (entry.name.startsWith("."))
|
|
375
|
+
continue;
|
|
376
|
+
const fullPath = path.join(dir, entry.name);
|
|
377
|
+
if (entry.isDirectory())
|
|
378
|
+
files.push(...collectMarkdownFiles(fullPath));
|
|
379
|
+
else if (entry.isFile() && entry.name.endsWith(".md"))
|
|
380
|
+
files.push(fullPath);
|
|
381
|
+
}
|
|
382
|
+
return files;
|
|
383
|
+
}
|
|
384
|
+
function loadSkillEntry(filePath) {
|
|
385
|
+
try {
|
|
386
|
+
const content = readFileSync(filePath, "utf8");
|
|
387
|
+
const { frontmatter, body } = parseMarkdownDefinition(content);
|
|
388
|
+
const metadata = frontmatter;
|
|
389
|
+
const name = typeof metadata.name === "string" ? metadata.name.trim() : "";
|
|
390
|
+
if (!name)
|
|
391
|
+
return undefined;
|
|
392
|
+
const description = typeof metadata.description === "string"
|
|
393
|
+
? metadata.description.trim()
|
|
394
|
+
: firstParagraph(body);
|
|
395
|
+
return { name, description, location: filePath };
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function firstParagraph(text) {
|
|
402
|
+
return String(text ?? "")
|
|
403
|
+
.split(/\r?\n\r?\n/, 1)[0]
|
|
404
|
+
.replace(/^#+\s*/, "")
|
|
405
|
+
.replace(/\s+/g, " ")
|
|
406
|
+
.trim();
|
|
407
|
+
}
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small shared primitives with no Pi runtime dependency.
|
|
3
|
+
*
|
|
4
|
+
* These helpers are safe to reuse from policy, prompt, session, and run code.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isRecord(value: unknown): value is AnyRecord;
|
|
7
|
+
export declare function toStringArray(value: any): string[];
|
|
8
|
+
export declare function getErrorMessage(error: any): string;
|
|
9
|
+
export declare function parseIntegerRadius(value: any, fieldName: any, fallback?: number): number;
|
|
10
|
+
export declare function chooseBoolean(value: any, fallback: any): any;
|
|
11
|
+
export declare function formatDuration(milliseconds: any): string;
|
|
12
|
+
/** Resolves Pi-style include and exclude filters into a stable ordered list. */
|
|
13
|
+
export declare function applyFilterList(allNames: string[], filters?: string[]): string[];
|
|
14
|
+
export declare function mergeFilterLayers(...layers: unknown[]): string[];
|
|
15
|
+
export declare function coalesce(value: any, fallback: any): any;
|
|
16
|
+
export declare function shortSessionId(sessionId: any): string;
|
|
17
|
+
/** Creates the message visible to a target session when another session contacts it. */
|
|
18
|
+
export declare function buildReceiptText(callerAgent: any, callerSessionId: any, message: any): string;
|
|
19
|
+
export declare function buildReturnText(agent: any, sessionId: any, finalAnswer: any): string;
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small shared primitives with no Pi runtime dependency.
|
|
3
|
+
*
|
|
4
|
+
* These helpers are safe to reuse from policy, prompt, session, and run code.
|
|
5
|
+
*/
|
|
6
|
+
const FILTER_ALL = ["*"];
|
|
7
|
+
export function isRecord(value) {
|
|
8
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
export function toStringArray(value) {
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return value.filter((item) => typeof item === "string");
|
|
13
|
+
if (typeof value === "string")
|
|
14
|
+
return value
|
|
15
|
+
.split(",")
|
|
16
|
+
.map((item) => item.trim())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
export function getErrorMessage(error) {
|
|
21
|
+
return error instanceof Error ? error.message : String(error);
|
|
22
|
+
}
|
|
23
|
+
export function parseIntegerRadius(value, fieldName, fallback = 0) {
|
|
24
|
+
if (value === undefined || value === null)
|
|
25
|
+
return fallback;
|
|
26
|
+
const number = typeof value === "number" ? value : Number(value);
|
|
27
|
+
if (!Number.isFinite(number) || number < 0)
|
|
28
|
+
throw new Error(`${fieldName} must be a non-negative number.`);
|
|
29
|
+
return Math.floor(number);
|
|
30
|
+
}
|
|
31
|
+
export function chooseBoolean(value, fallback) {
|
|
32
|
+
return typeof value === "boolean" ? value : fallback;
|
|
33
|
+
}
|
|
34
|
+
export function formatDuration(milliseconds) {
|
|
35
|
+
const totalSeconds = Math.max(0, Math.floor(milliseconds / 1000));
|
|
36
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
37
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
38
|
+
const seconds = totalSeconds % 60;
|
|
39
|
+
if (hours > 0)
|
|
40
|
+
return `${hours}h:${minutes.toString().padStart(2, "0")}m:${seconds.toString().padStart(2, "0")}s`;
|
|
41
|
+
if (minutes > 0)
|
|
42
|
+
return `${minutes}m:${seconds.toString().padStart(2, "0")}s`;
|
|
43
|
+
return `${seconds}s`;
|
|
44
|
+
}
|
|
45
|
+
function wildcardToRegExp(pattern) {
|
|
46
|
+
const escaped = pattern
|
|
47
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
48
|
+
.replace(/\*/g, ".*")
|
|
49
|
+
.replace(/\?/g, ".");
|
|
50
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
51
|
+
}
|
|
52
|
+
function matchesPattern(name, pattern) {
|
|
53
|
+
if (pattern === "*")
|
|
54
|
+
return true;
|
|
55
|
+
if (pattern.includes("*") || pattern.includes("?"))
|
|
56
|
+
return wildcardToRegExp(pattern).test(name);
|
|
57
|
+
return name.toLowerCase().includes(pattern.toLowerCase());
|
|
58
|
+
}
|
|
59
|
+
/** Resolves Pi-style include and exclude filters into a stable ordered list. */
|
|
60
|
+
export function applyFilterList(allNames, filters = FILTER_ALL) {
|
|
61
|
+
if (!Array.isArray(filters))
|
|
62
|
+
return [...allNames];
|
|
63
|
+
if (filters.length === 0)
|
|
64
|
+
return [];
|
|
65
|
+
const includes = [];
|
|
66
|
+
const excludes = [];
|
|
67
|
+
const forceIncludes = [];
|
|
68
|
+
const forceExcludes = [];
|
|
69
|
+
for (const raw of filters) {
|
|
70
|
+
if (typeof raw !== "string" || !raw)
|
|
71
|
+
continue;
|
|
72
|
+
if (raw.startsWith("+"))
|
|
73
|
+
forceIncludes.push(raw.slice(1));
|
|
74
|
+
else if (raw.startsWith("-"))
|
|
75
|
+
forceExcludes.push(raw.slice(1));
|
|
76
|
+
else if (raw.startsWith("!"))
|
|
77
|
+
excludes.push(raw.slice(1));
|
|
78
|
+
else
|
|
79
|
+
includes.push(raw);
|
|
80
|
+
}
|
|
81
|
+
const selected = new Set(includes.length === 0
|
|
82
|
+
? allNames
|
|
83
|
+
: allNames.filter((name) => includes.some((p) => matchesPattern(name, p))));
|
|
84
|
+
for (const name of [...selected]) {
|
|
85
|
+
if (excludes.some((p) => matchesPattern(name, p)))
|
|
86
|
+
selected.delete(name);
|
|
87
|
+
}
|
|
88
|
+
for (const name of allNames) {
|
|
89
|
+
if (forceIncludes.some((p) => p.toLowerCase() === name.toLowerCase()))
|
|
90
|
+
selected.add(name);
|
|
91
|
+
}
|
|
92
|
+
for (const name of allNames) {
|
|
93
|
+
if (forceExcludes.some((p) => p.toLowerCase() === name.toLowerCase()))
|
|
94
|
+
selected.delete(name);
|
|
95
|
+
}
|
|
96
|
+
return allNames.filter((name) => selected.has(name));
|
|
97
|
+
}
|
|
98
|
+
export function mergeFilterLayers(...layers) {
|
|
99
|
+
const result = [];
|
|
100
|
+
for (const layer of layers) {
|
|
101
|
+
if (layer === undefined)
|
|
102
|
+
continue;
|
|
103
|
+
if (!Array.isArray(layer))
|
|
104
|
+
continue;
|
|
105
|
+
if (layer.length === 0)
|
|
106
|
+
return [];
|
|
107
|
+
result.push(...layer);
|
|
108
|
+
}
|
|
109
|
+
return result.length === 0 ? undefined : result;
|
|
110
|
+
}
|
|
111
|
+
export function coalesce(value, fallback) {
|
|
112
|
+
return value === undefined ? fallback : value;
|
|
113
|
+
}
|
|
114
|
+
export function shortSessionId(sessionId) {
|
|
115
|
+
return String(sessionId ?? "").slice(0, 8);
|
|
116
|
+
}
|
|
117
|
+
/** Creates the message visible to a target session when another session contacts it. */
|
|
118
|
+
export function buildReceiptText(callerAgent, callerSessionId, message) {
|
|
119
|
+
const agentText = callerAgent ? `[${callerAgent}] agent` : "agent";
|
|
120
|
+
return `Message from ${agentText} from session ${shortSessionId(callerSessionId)}:\n${message}\nOnly your final answer will be returned.`;
|
|
121
|
+
}
|
|
122
|
+
export function buildReturnText(agent, sessionId, finalAnswer) {
|
|
123
|
+
const agentText = agent ? `[${agent}] agent` : "agent";
|
|
124
|
+
return `Message from ${agentText} from session ${shortSessionId(sessionId)}:\n${finalAnswer}`;
|
|
125
|
+
}
|