@anhlp/alp-core 0.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +7 -0
- package/README.md +7 -0
- package/dist/adapter.d.ts +20 -0
- package/dist/adapter.js +30 -0
- package/dist/atomic.d.ts +2 -0
- package/dist/atomic.js +28 -0
- package/dist/backup.d.ts +27 -0
- package/dist/backup.js +582 -0
- package/dist/beads.d.ts +65 -0
- package/dist/beads.js +213 -0
- package/dist/delegation.d.ts +2 -0
- package/dist/delegation.js +32 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +11 -0
- package/dist/formulas.d.ts +25 -0
- package/dist/formulas.js +201 -0
- package/dist/hook-run.d.ts +44 -0
- package/dist/hook-run.js +60 -0
- package/dist/hooks.d.ts +35 -0
- package/dist/hooks.js +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +63 -0
- package/dist/init.d.ts +30 -0
- package/dist/init.js +88 -0
- package/dist/library-edit.d.ts +90 -0
- package/dist/library-edit.js +315 -0
- package/dist/library.d.ts +28 -0
- package/dist/library.js +108 -0
- package/dist/log.d.ts +37 -0
- package/dist/log.js +156 -0
- package/dist/migrations.d.ts +9 -0
- package/dist/migrations.js +95 -0
- package/dist/models.d.ts +11 -0
- package/dist/models.js +44 -0
- package/dist/permissions.d.ts +14 -0
- package/dist/permissions.js +229 -0
- package/dist/profiles.d.ts +27 -0
- package/dist/profiles.js +122 -0
- package/dist/promptsafe.d.ts +1 -0
- package/dist/promptsafe.js +13 -0
- package/dist/providers.d.ts +90 -0
- package/dist/providers.js +101 -0
- package/dist/resolver.d.ts +41 -0
- package/dist/resolver.js +240 -0
- package/dist/safe-path.d.ts +2 -0
- package/dist/safe-path.js +24 -0
- package/dist/shell.d.ts +16 -0
- package/dist/shell.js +22 -0
- package/dist/state-schema.d.ts +9 -0
- package/dist/state-schema.js +54 -0
- package/dist/task-rows.d.ts +47 -0
- package/dist/task-rows.js +45 -0
- package/dist/tasks.d.ts +128 -0
- package/dist/tasks.js +835 -0
- package/dist/teams.d.ts +69 -0
- package/dist/teams.js +159 -0
- package/dist/template-path.d.ts +2 -0
- package/dist/template-path.js +10 -0
- package/dist/trust.d.ts +8 -0
- package/dist/trust.js +47 -0
- package/dist/types.d.ts +51 -0
- package/dist/upgrade.d.ts +10 -0
- package/dist/upgrade.js +200 -0
- package/dist/user-settings.d.ts +6 -0
- package/dist/user-settings.js +72 -0
- package/dist/validation.d.ts +17 -0
- package/dist/validation.js +295 -0
- package/dist/verify.d.ts +15 -0
- package/dist/verify.js +123 -0
- package/dist/workflow.d.ts +14 -0
- package/dist/workflow.js +24 -0
- package/package.json +309 -0
- package/src/adapter.d.ts +20 -0
- package/src/adapter.js +29 -0
- package/src/atomic.d.ts +2 -0
- package/src/atomic.js +28 -0
- package/src/backup.d.ts +27 -0
- package/src/backup.js +570 -0
- package/src/beads.js +236 -0
- package/src/delegation.js +28 -0
- package/src/errors.js +7 -0
- package/src/formulas.d.ts +25 -0
- package/src/formulas.js +202 -0
- package/src/hook-run.js +57 -0
- package/src/hooks.js +36 -0
- package/src/index.js +31 -0
- package/src/init.js +92 -0
- package/src/library-edit.js +346 -0
- package/src/library.js +117 -0
- package/src/log.d.ts +37 -0
- package/src/log.js +177 -0
- package/src/migrations.d.ts +9 -0
- package/src/migrations.js +111 -0
- package/src/models.js +45 -0
- package/src/permissions.d.ts +14 -0
- package/src/permissions.js +242 -0
- package/src/profiles.js +139 -0
- package/src/promptsafe.d.ts +1 -0
- package/src/promptsafe.js +19 -0
- package/src/providers.js +122 -0
- package/src/resolver.js +238 -0
- package/src/safe-path.js +20 -0
- package/src/shell.js +28 -0
- package/src/state-schema.d.ts +9 -0
- package/src/state-schema.js +60 -0
- package/src/task-rows.js +44 -0
- package/src/tasks.d.ts +128 -0
- package/src/tasks.js +881 -0
- package/src/teams.js +170 -0
- package/src/template-path.js +8 -0
- package/src/trust.js +59 -0
- package/src/types.d.ts +51 -0
- package/src/upgrade.js +195 -0
- package/src/user-settings.d.ts +6 -0
- package/src/user-settings.js +76 -0
- package/src/validation.d.ts +17 -0
- package/src/validation.js +302 -0
- package/src/verify.d.ts +15 -0
- package/src/verify.js +127 -0
- package/src/workflow.js +28 -0
- package/templates/ALP.md +52 -0
- package/templates/agents/lead/AGENT.md +90 -0
- package/templates/agents/main/AGENT.md +150 -0
- package/templates/agents/oracle/AGENT.md +44 -0
- package/templates/agents/peer/AGENT.md +55 -0
- package/templates/agents/reviewer/AGENT.md +80 -0
- package/templates/agents/supervisor/AGENT.md +72 -0
- package/templates/providers/antigravity.json +6 -0
- package/templates/providers/opencode.json +7 -0
- package/templates/role-skills.json +8 -0
- package/templates/skills/bug-loop/SKILL.md +44 -0
- package/templates/skills/bug-loop/references/test-proof.md +25 -0
- package/templates/skills/goal-griller/SKILL.md +49 -0
- package/templates/skills/prompt-leverage/SKILL.md +37 -0
- package/templates/skills/prompt-leverage/references/brief-template.md +27 -0
- package/templates/skills/sequence-execution-plan/SKILL.md +42 -0
- package/templates/skills/sequence-execution-plan/references/plan-template.md +22 -0
- package/templates/skills/smart-commits/SKILL.md +40 -0
- package/templates/skills/xia/SKILL.md +42 -0
- package/templates/teams/cafe/HOUSE_RULES.md +1 -0
- package/templates/teams/cafe/team.json +15 -0
- package/templates/teams/pho/HOUSE_RULES.md +1 -0
- package/templates/teams/pho/team.json +14 -0
package/src/init.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { shippedTemplateRoot } from './template-path.js';
|
|
2
|
+
import { mkdir, writeFile, lstat, readFile, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { projectProfiles, profileDefaults, syncTemplateProfiles } from './profiles.js';
|
|
6
|
+
import { validateSettings } from './validation.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Fill missing scaffold files without replacing existing user content: ALP.md and
|
|
10
|
+
* .alp/settings.json. Agents, skills, MCP servers and hooks come from ALP's built-ins
|
|
11
|
+
* and the user's library (ALPD §41); a project adds its own under .alp/ only to
|
|
12
|
+
* override them.
|
|
13
|
+
* @param {string} projectRoot
|
|
14
|
+
* @param {{ templateRoot?: URL, templates?: Record<string, string>, library?: string, syncProfiles?: boolean, onlyManagedProfiles?: boolean, teamIds?: string[] }} [options]
|
|
15
|
+
*/
|
|
16
|
+
export async function initProject(projectRoot, { templateRoot, templates, library, syncProfiles = true, onlyManagedProfiles = false, teamIds } = {}) {
|
|
17
|
+
const root = path.resolve(projectRoot);
|
|
18
|
+
const sourceRoot = () => templateRoot ?? shippedTemplateRoot();
|
|
19
|
+
const template = name => templates ? Promise.resolve(templates[name]) : readFile(new URL(name, sourceRoot()), 'utf8');
|
|
20
|
+
const scaffold = path.join(root, '.alp');
|
|
21
|
+
const existing = await lstat(scaffold).catch(error => { if (error.code !== 'ENOENT') throw error; });
|
|
22
|
+
if (existing && !existing.isDirectory()) throw new Error(`Expected a directory: ${scaffold}`);
|
|
23
|
+
// Load the complete starter before modifying the destination.
|
|
24
|
+
const team = await projectProfiles({ root, templates, library, teamIds });
|
|
25
|
+
const files = {
|
|
26
|
+
'ALP.md': await template('ALP.md'),
|
|
27
|
+
'.alp/settings.json': JSON.stringify({ defaultAgent: 'main', workflow: { mode: 'pho' }, team, templateDefaults: profileDefaults(team) }, null, 2) + '\n',
|
|
28
|
+
};
|
|
29
|
+
const directories = ['.alp'];
|
|
30
|
+
const created = [];
|
|
31
|
+
const preserved = [];
|
|
32
|
+
const updated = [];
|
|
33
|
+
await mkdir(root, { recursive: true });
|
|
34
|
+
for (const directory of directories) {
|
|
35
|
+
const target = path.join(root, directory);
|
|
36
|
+
try {
|
|
37
|
+
await mkdir(target);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error.code !== 'EEXIST') throw error;
|
|
40
|
+
if (!(await lstat(target)).isDirectory()) throw new Error(`Expected a directory: ${target}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const [name, content] of Object.entries(files)) {
|
|
44
|
+
const target = path.join(root, name);
|
|
45
|
+
try {
|
|
46
|
+
await writeFile(target, content, { flag: 'wx' });
|
|
47
|
+
created.push(name);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error.code !== 'EEXIST') throw error;
|
|
50
|
+
if (!(await lstat(target)).isFile()) throw new Error(`Expected a regular file: ${target}`);
|
|
51
|
+
preserved.push(name);
|
|
52
|
+
if (name === '.alp/settings.json' && syncProfiles) {
|
|
53
|
+
const before = await readFile(target, 'utf8');
|
|
54
|
+
const settings = JSON.parse(before);
|
|
55
|
+
validateSettings(settings, target);
|
|
56
|
+
const next = !onlyManagedProfiles || settings.team || settings.profile || settings.templateDefaults ? await syncTemplateProfiles(settings, { root, library, templates, teamIds }) : settings;
|
|
57
|
+
validateSettings(next, target);
|
|
58
|
+
if (JSON.stringify(next) !== JSON.stringify(settings)) {
|
|
59
|
+
if (await readFile(target, 'utf8') !== before) throw new Error(`File changed during initialization: ${target}`);
|
|
60
|
+
await writeFile(target, JSON.stringify(next, null, 2) + '\n');
|
|
61
|
+
updated.push(name);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { created, preserved, ...(updated.length ? { updated } : {}) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Refresh an existing project's settings only; a template Save must not scaffold unrelated folders.
|
|
70
|
+
* @param {string} projectRoot
|
|
71
|
+
* @param {{ library?: string, templates?: Record<string, string>, teamIds?: string[], replaceModels?: boolean }} [options]
|
|
72
|
+
*/
|
|
73
|
+
export async function syncProjectSettings(projectRoot, { library, templates, teamIds, replaceModels = false } = {}) {
|
|
74
|
+
const root = path.resolve(projectRoot);
|
|
75
|
+
const file = path.join(root, '.alp/settings.json');
|
|
76
|
+
const info = await lstat(file).catch(error => { if (error.code !== 'ENOENT') throw error; });
|
|
77
|
+
if (!info) return false;
|
|
78
|
+
if (!info.isFile()) throw new Error(`Expected a regular file: ${file}`);
|
|
79
|
+
const before = await readFile(file, 'utf8');
|
|
80
|
+
const settings = JSON.parse(before);
|
|
81
|
+
validateSettings(settings, file);
|
|
82
|
+
const next = await syncTemplateProfiles(settings, { root, library, templates, teamIds, replaceModels });
|
|
83
|
+
validateSettings(next, file);
|
|
84
|
+
if (JSON.stringify(settings) === JSON.stringify(next)) return false;
|
|
85
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
86
|
+
try {
|
|
87
|
+
await writeFile(temporary, JSON.stringify(next, null, 2) + '\n', { flag: 'wx', mode: info.mode });
|
|
88
|
+
if (await readFile(file, 'utf8') !== before) throw new Error(`Settings changed while saving the template: ${file}`);
|
|
89
|
+
await rename(temporary, file);
|
|
90
|
+
} finally { await rm(temporary, { force: true }); }
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { moveFile } from './atomic.js';
|
|
5
|
+
import { AlpError } from './errors.js';
|
|
6
|
+
import { assertNoSymlinks } from './safe-path.js';
|
|
7
|
+
import { agentSources, builtinText, jsonObject, libraryEntries, optionalText } from './resolver.js';
|
|
8
|
+
import { teamSources, validateTeam } from './teams.js';
|
|
9
|
+
import { normalizeMcp, validateAgentConfig, validateHook } from './validation.js';
|
|
10
|
+
import { validateProvider } from './providers.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Editing the library (ALPD §43): agents, skills, MCP servers, hooks and teams, in the
|
|
14
|
+
* user's library (ALP_HOME) or as a project's override (.alp/). Built-ins are never
|
|
15
|
+
* written; duplicate one, or save an entry of its name, to change it. Every save is
|
|
16
|
+
* checked first and written file by file through a temporary file and a rename; a
|
|
17
|
+
* revision taken when the entry was read refuses a save over someone else's change.
|
|
18
|
+
*
|
|
19
|
+
* An entry's content, by kind:
|
|
20
|
+
* agents { instructions, config? } AGENT.md and agent.json
|
|
21
|
+
* skills { body } SKILL.md
|
|
22
|
+
* mcp { server } mcp/<name>.json
|
|
23
|
+
* hooks { hook } hooks/<name>.json
|
|
24
|
+
* teams { team, houseRules? } team.json and HOUSE_RULES.md
|
|
25
|
+
* providers { provider } providers/<name>.json, in the library only (ALPD §46)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export const EDIT_KINDS = ['agents', 'skills', 'mcp', 'hooks', 'teams', 'providers'];
|
|
29
|
+
const validName = name => typeof name === 'string' && /^[\w.-]+$/.test(name) && name !== '.' && name !== '..';
|
|
30
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
31
|
+
const fail = (code, message) => { throw new AlpError(code, message); };
|
|
32
|
+
|
|
33
|
+
/** The files an entry has, relative to its kind's directory, and what each holds. */
|
|
34
|
+
const FILES = {
|
|
35
|
+
agents: name => [[`${name}/AGENT.md`, 'instructions', 'text'], [`${name}/agent.json`, 'config', 'json']],
|
|
36
|
+
skills: name => [[`${name}/SKILL.md`, 'body', 'text']],
|
|
37
|
+
mcp: name => [[`${name}.json`, 'server', 'json']],
|
|
38
|
+
hooks: name => [[`${name}.json`, 'hook', 'json']],
|
|
39
|
+
teams: name => [[`${name}/team.json`, 'team', 'json'], [`${name}/HOUSE_RULES.md`, 'houseRules', 'text']],
|
|
40
|
+
providers: name => [[`${name}.json`, 'provider', 'json']],
|
|
41
|
+
};
|
|
42
|
+
/** The directory an entry owns, if any: removed whole on delete and moved whole on rename. */
|
|
43
|
+
const ownDirectory = kind => ['agents', 'skills', 'teams'].includes(kind);
|
|
44
|
+
|
|
45
|
+
function kindOf(kind) {
|
|
46
|
+
if (!EDIT_KINDS.includes(kind)) fail('INVALID_KIND', `Unknown kind '${kind}'; use one of ${EDIT_KINDS.join(', ')}`);
|
|
47
|
+
return kind;
|
|
48
|
+
}
|
|
49
|
+
function nameOf(name) {
|
|
50
|
+
if (!validName(name)) fail('INVALID_NAME', `'${name}' is not a valid name: use letters, digits, '.', '_' or '-'`);
|
|
51
|
+
return name;
|
|
52
|
+
}
|
|
53
|
+
/** The directory a scope writes to. */
|
|
54
|
+
function base(scope, { root, library }, kind) {
|
|
55
|
+
// A project's provider would run a command from its repository on the user's machine.
|
|
56
|
+
if (kind === 'providers' && scope !== 'library') fail('INVALID_SCOPE', 'Providers live only in your library');
|
|
57
|
+
if (scope === 'library') return library ?? fail('INVALID_SCOPE', 'No library: ALP_HOME is not set');
|
|
58
|
+
if (scope === 'project') return root ? path.join(path.resolve(root), '.alp') : fail('INVALID_SCOPE', 'No project directory');
|
|
59
|
+
return fail('INVALID_SCOPE', `Scope must be library or project, not '${scope}'`);
|
|
60
|
+
}
|
|
61
|
+
const revisionOf = content => content === undefined ? null : createHash('sha256').update(JSON.stringify(content)).digest('hex').slice(0, 16);
|
|
62
|
+
|
|
63
|
+
/** Reject repository-controlled links before any entry read or mutation. This is
|
|
64
|
+
* a preflight for static repository input, not a sandbox against same-user races. */
|
|
65
|
+
async function safeEntryPaths(directory, kind, name) {
|
|
66
|
+
for (const [file] of FILES[kind](name)) {
|
|
67
|
+
await assertNoSymlinks(directory, path.join(kind, file));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** An entry's content as one layer holds it, or undefined when that layer has none. */
|
|
72
|
+
async function readLayer(kind, name, directory) {
|
|
73
|
+
await safeEntryPaths(directory, kind, name);
|
|
74
|
+
const content = {};
|
|
75
|
+
let found = false;
|
|
76
|
+
for (const [file, key, type] of FILES[kind](name)) {
|
|
77
|
+
const target = path.join(directory, kind, file);
|
|
78
|
+
const text = await optionalText(target);
|
|
79
|
+
if (text === undefined) continue;
|
|
80
|
+
found = true;
|
|
81
|
+
if (type === 'text') { content[key] = text; continue; }
|
|
82
|
+
try { content[key] = JSON.parse(text); }
|
|
83
|
+
catch (cause) { throw new AlpError('INVALID_ENTRY', `${target}: ${cause.message}`, { cause }); }
|
|
84
|
+
}
|
|
85
|
+
// An agent or skill is its instructions; a team its team.json.
|
|
86
|
+
const main = { agents: 'instructions', skills: 'body', teams: 'team' }[kind];
|
|
87
|
+
return found && (!main || content[main] !== undefined) ? content : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A built-in entry's content: the agents and teams ALP ships. */
|
|
91
|
+
async function readBuiltin(kind, name, templates) {
|
|
92
|
+
if (kind === 'agents') {
|
|
93
|
+
const instructions = await builtinText(`agents/${name}/AGENT.md`, templates);
|
|
94
|
+
if (instructions === undefined) return undefined;
|
|
95
|
+
const config = await builtinText(`agents/${name}/agent.json`, templates);
|
|
96
|
+
return { instructions, ...(config !== undefined ? { config: JSON.parse(config) } : {}) };
|
|
97
|
+
}
|
|
98
|
+
if (kind === 'teams') {
|
|
99
|
+
const team = await builtinText(`teams/${name}/team.json`, templates);
|
|
100
|
+
if (team === undefined) return undefined;
|
|
101
|
+
return { team: JSON.parse(team), houseRules: (await builtinText(`teams/${name}/HOUSE_RULES.md`, templates)) ?? '' };
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Each layer's version of an entry, in order: built-in, library, project. */
|
|
107
|
+
async function layers(kind, name, { root, library, templates }) {
|
|
108
|
+
return {
|
|
109
|
+
builtin: await readBuiltin(kind, name, templates),
|
|
110
|
+
library: library ? await readLayer(kind, name, library) : undefined,
|
|
111
|
+
project: root && kind !== 'providers' ? await readLayer(kind, name, path.join(path.resolve(root), '.alp')) : undefined,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* One entry: the version that applies (the project's, else the library's, else the
|
|
117
|
+
* built-in), or one scope's version with `scope`. `revision` goes back with a save.
|
|
118
|
+
* @param {'agents' | 'skills' | 'mcp' | 'hooks' | 'teams' | 'providers'} kind
|
|
119
|
+
* @param {string} name
|
|
120
|
+
* @param {{ root?: string, library?: string, templates?: Record<string, string>, scope?: 'library' | 'project' }} [options]
|
|
121
|
+
* @returns {Promise<{ kind: 'agents' | 'skills' | 'mcp' | 'hooks' | 'teams' | 'providers', name: string, source: 'builtin' | 'library' | 'project', overrides?: 'builtin' | 'library' | 'project', content: Record<string, unknown>, revision: string | null, usedBy: string[] }>}
|
|
122
|
+
*/
|
|
123
|
+
export async function getEntry(kind, name, { root, library, templates, scope } = {}) {
|
|
124
|
+
kindOf(kind); nameOf(name);
|
|
125
|
+
const found = await layers(kind, name, { root, library, templates });
|
|
126
|
+
const order = scope ? [scope] : ['project', 'library', 'builtin'];
|
|
127
|
+
const source = order.find(layer => found[layer] !== undefined);
|
|
128
|
+
if (!source) fail('NOT_FOUND', `No ${kind} entry '${name}'${scope ? ` in the ${scope}` : ''}`);
|
|
129
|
+
const below = ['builtin', 'library', 'project'].slice(0, ['builtin', 'library', 'project'].indexOf(source)).reverse().find(layer => found[layer] !== undefined);
|
|
130
|
+
return {
|
|
131
|
+
kind, name, source: /** @type {'builtin' | 'library' | 'project'} */ (source), ...(below ? { overrides: /** @type {'builtin' | 'library'} */ (below) } : {}), content: found[source], revision: revisionOf(found[source]), usedBy: await usersOf(kind, name, { root, library, templates }),
|
|
132
|
+
// The skills the library gives an agent by name, whatever layer defines it (ALPD §49).
|
|
133
|
+
...(kind === 'agents' && library ? { librarySkills: await givenSkills(library, name) } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** role-skills.json: the library's skills for each agent, by name. */
|
|
138
|
+
async function readGiven(library) {
|
|
139
|
+
const file = path.join(library, 'role-skills.json');
|
|
140
|
+
const text = await optionalText(file);
|
|
141
|
+
if (text === undefined) return { file, roles: {} };
|
|
142
|
+
let roles;
|
|
143
|
+
try { roles = JSON.parse(text); }
|
|
144
|
+
catch (cause) { throw new AlpError('INVALID_LIBRARY', `${file}: ${cause.message}`, { cause }); }
|
|
145
|
+
if (!object(roles)) fail('INVALID_LIBRARY', `${file}: expected an object of agent names to skill lists`);
|
|
146
|
+
return { file, roles };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The skills the library gives `agent`, as role-skills.json lists them. */
|
|
150
|
+
export async function givenSkills(library, agent) {
|
|
151
|
+
const { roles } = await readGiven(library);
|
|
152
|
+
return Array.isArray(roles[agent]) ? roles[agent].filter(name => typeof name === 'string') : [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Sets the skills the library gives an agent (role-skills.json), so a built-in agent's
|
|
157
|
+
* skills change without copying its instructions. Every skill must be in the library.
|
|
158
|
+
* @returns {Promise<{ skills: string[] }>}
|
|
159
|
+
*/
|
|
160
|
+
export async function setGivenSkills(agent, skills, { library } = {}) {
|
|
161
|
+
nameOf(agent);
|
|
162
|
+
if (!library) fail('INVALID_SCOPE', 'No library: ALP_HOME is not set');
|
|
163
|
+
if (!Array.isArray(skills) || !skills.every(validName)) fail('INVALID_ENTRY', 'Skills must be a list of skill names');
|
|
164
|
+
const unique = [...new Set(skills)];
|
|
165
|
+
for (const skill of unique) if (!(await exists(path.join(library, 'skills', skill, 'SKILL.md')))) fail('NOT_FOUND', `No skill '${skill}' in the library`);
|
|
166
|
+
const { file, roles } = await readGiven(library);
|
|
167
|
+
if (unique.length) roles[agent] = unique; else delete roles[agent];
|
|
168
|
+
await writeAtomic(file, JSON.stringify(roles, null, 2) + '\n');
|
|
169
|
+
return { skills: unique };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Every entry of a kind with its source and users, as `alp <kind>` lists them. */
|
|
173
|
+
export async function listEntries(kind, { root, library, templates } = {}) {
|
|
174
|
+
kindOf(kind);
|
|
175
|
+
if (kind !== 'teams') return libraryEntries(kind, root, { library, templates });
|
|
176
|
+
const rows = [];
|
|
177
|
+
for (const [name, source] of await teamSources(root, { library, templates })) rows.push({ name, source: source.source, ...(source.overrides ? { overrides: source.overrides } : {}), ...(source.directory ? { path: source.directory } : {}), usedBy: await usersOf(kind, name, { root, library, templates }) });
|
|
178
|
+
return rows;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Who refers to an entry: agents naming it, teams with it as a member, settings and role-skills.json. */
|
|
182
|
+
export async function usersOf(kind, name, { root, library, templates } = {}) {
|
|
183
|
+
const users = new Set();
|
|
184
|
+
if (kind === 'providers' && root) {
|
|
185
|
+
const settings = await jsonObject(path.join(path.resolve(root), '.alp', 'settings.json'), 'INVALID_SETTINGS').catch(() => ({}));
|
|
186
|
+
if (settings.runtime?.provider === name) users.add('project settings');
|
|
187
|
+
}
|
|
188
|
+
if (kind === 'teams') {
|
|
189
|
+
if (root) {
|
|
190
|
+
const settings = await jsonObject(path.join(path.resolve(root), '.alp', 'settings.json'), 'INVALID_SETTINGS').catch(() => ({}));
|
|
191
|
+
if (settings.workflow?.mode === name) users.add('project settings');
|
|
192
|
+
}
|
|
193
|
+
return [...users];
|
|
194
|
+
}
|
|
195
|
+
if (kind === 'agents') {
|
|
196
|
+
for (const [team, source] of await teamSources(root, { library, templates })) {
|
|
197
|
+
const text = source.directory ? await optionalText(path.join(source.directory, 'team.json')) : await builtinText(`teams/${team}/team.json`, templates);
|
|
198
|
+
try {
|
|
199
|
+
const config = JSON.parse(text ?? '{}');
|
|
200
|
+
if (config.main === name || Object.hasOwn(config.members ?? {}, name) || config.supervisor?.agent === name) users.add(`team ${team}`);
|
|
201
|
+
} catch {}
|
|
202
|
+
}
|
|
203
|
+
if (root) {
|
|
204
|
+
const settings = await jsonObject(path.join(path.resolve(root), '.alp', 'settings.json'), 'INVALID_SETTINGS').catch(() => ({}));
|
|
205
|
+
if (settings.defaultAgent === name) users.add('project settings');
|
|
206
|
+
for (const [owner, targets] of Object.entries(object(settings.delegation) ? settings.delegation : {})) if (owner === name || (Array.isArray(targets) && targets.includes(name))) users.add('project delegation');
|
|
207
|
+
}
|
|
208
|
+
return [...users].sort();
|
|
209
|
+
}
|
|
210
|
+
// Agents naming it in agent.json, and for skills the roles role-skills.json gives it.
|
|
211
|
+
const rows = await libraryEntries(kind, root, { library, templates });
|
|
212
|
+
for (const user of rows.find(row => row.name === name)?.usedBy ?? []) users.add(`agent ${user}`);
|
|
213
|
+
return [...users].sort();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Checks content for a kind, and that what it names exists where the entry will live. */
|
|
217
|
+
async function check(kind, name, content, { scope, root, library, templates }) {
|
|
218
|
+
if (!object(content)) fail('INVALID_ENTRY', `${kind} '${name}': content must be an object`);
|
|
219
|
+
const where = `${scope} ${kind} '${name}'`;
|
|
220
|
+
const visible = scope === 'project' ? { root, library, templates } : { library, templates };
|
|
221
|
+
const text = (key, required) => {
|
|
222
|
+
if (content[key] === undefined && !required) return;
|
|
223
|
+
if (typeof content[key] !== 'string' || (required && !content[key].trim())) fail('INVALID_ENTRY', `${where}: ${key} must be ${required ? 'nonempty ' : ''}text`);
|
|
224
|
+
};
|
|
225
|
+
const only = keys => { for (const key of Object.keys(content)) if (!keys.includes(key)) fail('INVALID_ENTRY', `${where}: unknown field '${key}'; expected ${keys.join(', ')}`); };
|
|
226
|
+
if (kind === 'agents') {
|
|
227
|
+
only(['instructions', 'config']);
|
|
228
|
+
text('instructions', true);
|
|
229
|
+
if (content.config !== undefined) {
|
|
230
|
+
validateAgentConfig(content.config, `${where} agent.json`);
|
|
231
|
+
for (const [list, entries] of [['skills', 'skills'], ['mcp', 'mcp'], ['hooks', 'hooks']]) {
|
|
232
|
+
if (!content.config[list]?.length) continue;
|
|
233
|
+
const known = new Set((await libraryEntries(entries, visible.root, visible)).map(row => row.name));
|
|
234
|
+
for (const entry of content.config[list]) if (!known.has(entry)) fail('NOT_FOUND', `${where}: ${list} names '${entry}', which is not in the ${scope === 'project' ? 'project or the library' : 'library'}`);
|
|
235
|
+
}
|
|
236
|
+
const provider = content.config.provider;
|
|
237
|
+
if (provider !== undefined && !['codex', 'claude'].includes(provider) && !(await libraryEntries('providers', undefined, { library })).some(row => row.name === provider)) {
|
|
238
|
+
fail('NOT_FOUND', `${where}: provider '${provider}' is neither codex, claude nor an ACP provider in your library`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} else if (kind === 'skills') {
|
|
242
|
+
only(['body']);
|
|
243
|
+
text('body', true);
|
|
244
|
+
} else if (kind === 'mcp') {
|
|
245
|
+
only(['server']);
|
|
246
|
+
normalizeMcp({ mcpServers: { [name]: content.server } }, base(scope, { root, library }, kind), where);
|
|
247
|
+
} else if (kind === 'hooks') {
|
|
248
|
+
only(['hook']);
|
|
249
|
+
validateHook(content.hook, where);
|
|
250
|
+
} else if (kind === 'providers') {
|
|
251
|
+
only(['provider']);
|
|
252
|
+
validateProvider(content.provider, where, name);
|
|
253
|
+
} else {
|
|
254
|
+
only(['team', 'houseRules']);
|
|
255
|
+
text('houseRules', false);
|
|
256
|
+
validateTeam(content.team, `${where} team.json`);
|
|
257
|
+
const agents = await agentSources(visible.root, visible);
|
|
258
|
+
const supervisor = content.team.supervisor ? [content.team.supervisor.agent] : [];
|
|
259
|
+
for (const agent of [...Object.keys(content.team.members), ...supervisor]) if (!agents.has(agent)) fail('NOT_FOUND', `${where}: agent '${agent}' is not built in, nor in the ${scope === 'project' ? 'project or the library' : 'library'}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function writeAtomic(file, text) {
|
|
264
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
265
|
+
const temporary = `${file}.${randomUUID().slice(0, 8)}.tmp`;
|
|
266
|
+
await writeFile(temporary, text);
|
|
267
|
+
await moveFile(temporary, file); // retried on Windows while a reader has the target open
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const exists = target => stat(target).then(() => true, () => false);
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Creates or replaces an entry in a scope. `revision` is what getEntry returned for
|
|
274
|
+
* that scope (null to create); a different current revision refuses the save.
|
|
275
|
+
* @returns {Promise<{ revision: string | null }>}
|
|
276
|
+
*/
|
|
277
|
+
export async function saveEntry(kind, name, content, { scope, root, library, templates, revision } = {}) {
|
|
278
|
+
kindOf(kind); nameOf(name);
|
|
279
|
+
const directory = base(scope, { root, library }, kind);
|
|
280
|
+
await safeEntryPaths(directory, kind, name);
|
|
281
|
+
await check(kind, name, content, { scope, root, library, templates });
|
|
282
|
+
const current = await readLayer(kind, name, directory);
|
|
283
|
+
if (revision !== undefined && revision !== revisionOf(current)) {
|
|
284
|
+
fail('REVISION_CONFLICT', current === undefined ? `${kind} '${name}' was removed from the ${scope} since you opened it` : revision === null ? `${kind} '${name}' already exists in the ${scope}` : `${kind} '${name}' changed in the ${scope} since you opened it; reload it and apply your change again`);
|
|
285
|
+
}
|
|
286
|
+
if (scope === 'library') await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
287
|
+
for (const [file, key, type] of FILES[kind](name)) {
|
|
288
|
+
const target = path.join(directory, kind, file);
|
|
289
|
+
const value = content[key];
|
|
290
|
+
// An empty agent.json or house rules file is no file.
|
|
291
|
+
const empty = value === undefined || (type === 'json' && key === 'config' && !Object.keys(value).length) || (type === 'text' && key === 'houseRules' && !value.trim());
|
|
292
|
+
if (empty) { await rm(target, { force: true }); continue; }
|
|
293
|
+
await writeAtomic(target, type === 'json' ? JSON.stringify(value, null, 2) + '\n' : value.endsWith('\n') ? value : `${value}\n`);
|
|
294
|
+
}
|
|
295
|
+
return { revision: revisionOf(await readLayer(kind, name, directory)) };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Removes an entry from a scope. When nothing else of that name is left below it, an
|
|
300
|
+
* entry others refer to stays, and the error lists them.
|
|
301
|
+
* @returns {Promise<{ removed: boolean, now?: 'builtin' | 'library' }>}
|
|
302
|
+
*/
|
|
303
|
+
export async function deleteEntry(kind, name, { scope, root, library, templates, revision } = {}) {
|
|
304
|
+
kindOf(kind); nameOf(name);
|
|
305
|
+
const directory = base(scope, { root, library }, kind);
|
|
306
|
+
const current = await readLayer(kind, name, directory);
|
|
307
|
+
if (current === undefined) fail('NOT_FOUND', `No ${kind} entry '${name}' in the ${scope}`);
|
|
308
|
+
if (revision !== undefined && revision !== revisionOf(current)) fail('REVISION_CONFLICT', `${kind} '${name}' changed in the ${scope} since you opened it`);
|
|
309
|
+
const found = await layers(kind, name, { root: scope === 'project' ? root : undefined, library: scope === 'project' ? library : undefined, templates });
|
|
310
|
+
const fallback = scope === 'project' ? found.library ?? found.builtin : found.builtin;
|
|
311
|
+
if (fallback === undefined) {
|
|
312
|
+
const users = await usersOf(kind, name, { root, library, templates });
|
|
313
|
+
if (users.length) fail('IN_USE', `${kind} '${name}' is used by ${users.join(', ')}; change them first`);
|
|
314
|
+
}
|
|
315
|
+
if (ownDirectory(kind)) await rm(path.join(directory, kind, name), { recursive: true, force: true });
|
|
316
|
+
else for (const [file] of FILES[kind](name)) await rm(path.join(directory, kind, file), { force: true });
|
|
317
|
+
/** @type {'builtin' | 'library'} */
|
|
318
|
+
const now = scope === 'project' && found.library !== undefined ? 'library' : 'builtin';
|
|
319
|
+
return { removed: true, ...(fallback !== undefined ? { now } : {}) };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Copies the entry that applies under `from` to a new entry `to` in a scope; built-ins are copied this way. */
|
|
323
|
+
export async function duplicateEntry(kind, from, to, { scope, root, library, templates } = {}) {
|
|
324
|
+
const { content } = await getEntry(kind, from, { root, library, templates });
|
|
325
|
+
return saveEntry(kind, to, structuredClone(content), { scope, root, library, templates, revision: null });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Renames an entry within a scope; refused while others refer to it by its old name. */
|
|
329
|
+
export async function renameEntry(kind, from, to, { scope, root, library, templates } = {}) {
|
|
330
|
+
kindOf(kind); nameOf(from); nameOf(to);
|
|
331
|
+
const directory = base(scope, { root, library }, kind);
|
|
332
|
+
const content = await readLayer(kind, from, directory);
|
|
333
|
+
if (content === undefined) fail('NOT_FOUND', `No ${kind} entry '${from}' in the ${scope}`);
|
|
334
|
+
if (await readLayer(kind, to, directory) !== undefined) fail('REVISION_CONFLICT', `${kind} '${to}' already exists in the ${scope}`);
|
|
335
|
+
const users = await usersOf(kind, from, { root, library, templates });
|
|
336
|
+
if (users.length) fail('IN_USE', `${kind} '${from}' is used by ${users.join(', ')}; change them first`);
|
|
337
|
+
await check(kind, to, content, { scope, root, library, templates });
|
|
338
|
+
if (ownDirectory(kind)) {
|
|
339
|
+
if (await exists(path.join(directory, kind, to))) fail('REVISION_CONFLICT', `${path.join(directory, kind, to)} already exists`);
|
|
340
|
+
await rename(path.join(directory, kind, from), path.join(directory, kind, to));
|
|
341
|
+
} else {
|
|
342
|
+
await rename(path.join(directory, kind, `${from}.json`), path.join(directory, kind, `${to}.json`));
|
|
343
|
+
}
|
|
344
|
+
return { revision: revisionOf(await readLayer(kind, to, directory)) };
|
|
345
|
+
}
|
|
346
|
+
|
package/src/library.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { shippedTemplateRoot } from './template-path.js';
|
|
2
|
+
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { moveFile } from './atomic.js';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { AlpError } from './errors.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The user's library: skills, role assignments and default providers, kept in
|
|
11
|
+
* ALP_HOME and shared by every project. ALP seeds it from its templates once,
|
|
12
|
+
* then updates only files the user has not changed; a file the user edited or
|
|
13
|
+
* deleted stays that way across app updates.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** ALP_HOME, the user's ALP directory: alpd's home and the skill library. */
|
|
17
|
+
export function alpHome(env = process.env) {
|
|
18
|
+
return path.resolve(env.ALP_HOME || path.join(os.homedir(), '.alp'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const MANIFEST = 'library.json';
|
|
22
|
+
const sha = text => createHash('sha256').update(text.replaceAll('\r\n', '\n')).digest('hex');
|
|
23
|
+
|
|
24
|
+
async function optional(file) {
|
|
25
|
+
try { return await readFile(file, 'utf8'); }
|
|
26
|
+
catch (error) { if (error.code === 'ENOENT') return undefined; throw error; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Shipped library files: role skills, skill resources and default providers. */
|
|
30
|
+
async function shippedFiles({ templateRoot, templates }) {
|
|
31
|
+
if (templates) return Object.fromEntries(Object.entries(templates).filter(([name]) => name === 'role-skills.json' || name.startsWith('skills/') || name.startsWith('providers/')));
|
|
32
|
+
const root = templateRoot ?? shippedTemplateRoot();
|
|
33
|
+
const files = { 'role-skills.json': await readFile(new URL('role-skills.json', root), 'utf8') };
|
|
34
|
+
async function walk(relative) {
|
|
35
|
+
for (const entry of await readdir(new URL(`${relative}/`, root), { withFileTypes: true })) {
|
|
36
|
+
if (entry.isDirectory()) await walk(`${relative}/${entry.name}`);
|
|
37
|
+
else if (entry.isFile()) files[`${relative}/${entry.name}`] = await readFile(new URL(`${relative}/${entry.name}`, root), 'utf8');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
await walk('skills');
|
|
41
|
+
await walk('providers');
|
|
42
|
+
return files;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Seeds or updates the library in `home`. A file is written when ALP never
|
|
47
|
+
* shipped it before, and replaced only while it still holds what ALP last wrote.
|
|
48
|
+
* @param {string} home
|
|
49
|
+
* @param {{ templateRoot?: URL, templates?: Record<string, string> }} [options]
|
|
50
|
+
*/
|
|
51
|
+
export async function seedLibrary(home, options = {}) {
|
|
52
|
+
const shipped = await shippedFiles(options);
|
|
53
|
+
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
54
|
+
const manifestPath = path.join(home, MANIFEST);
|
|
55
|
+
const manifestText = await optional(manifestPath);
|
|
56
|
+
const manifest = manifestText ? JSON.parse(manifestText) : { files: {} };
|
|
57
|
+
const created = [];
|
|
58
|
+
const updated = [];
|
|
59
|
+
for (const [name, content] of Object.entries(shipped).sort(([a], [b]) => a.localeCompare(b))) {
|
|
60
|
+
const target = path.join(home, ...name.split('/'));
|
|
61
|
+
const current = await optional(target);
|
|
62
|
+
const seeded = manifest.files[name];
|
|
63
|
+
if (seeded === undefined) {
|
|
64
|
+
// A file the user made under a shipped name stays theirs.
|
|
65
|
+
if (current !== undefined) continue;
|
|
66
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
67
|
+
await writeFile(target, content, { flag: 'wx' }).catch(error => { if (error.code !== 'EEXIST') throw error; });
|
|
68
|
+
manifest.files[name] = sha(content);
|
|
69
|
+
created.push(name);
|
|
70
|
+
} else if (current !== undefined && sha(current) === seeded && seeded !== sha(content)) {
|
|
71
|
+
await writeFile(target, content);
|
|
72
|
+
manifest.files[name] = sha(content);
|
|
73
|
+
updated.push(name);
|
|
74
|
+
}
|
|
75
|
+
// Missing after seeding: the user deleted it. Changed: the user edited it. Both are kept.
|
|
76
|
+
}
|
|
77
|
+
if (created.length || updated.length || manifestText === undefined) {
|
|
78
|
+
await mkdir(home, { recursive: true });
|
|
79
|
+
const temporary = `${manifestPath}.${randomUUID().slice(0, 8)}.tmp`;
|
|
80
|
+
await writeFile(temporary, JSON.stringify(manifest, null, 2) + '\n');
|
|
81
|
+
await moveFile(temporary, manifestPath);
|
|
82
|
+
}
|
|
83
|
+
return { created, updated };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const seeding = new Map();
|
|
87
|
+
|
|
88
|
+
/** Seeds a library once per process; sessions call it on every open. */
|
|
89
|
+
export function ensureLibrary(home, options = {}) {
|
|
90
|
+
let done = seeding.get(home);
|
|
91
|
+
if (!done) {
|
|
92
|
+
done = seedLibrary(home, options).catch(error => { seeding.delete(home); throw error; });
|
|
93
|
+
seeding.set(home, done);
|
|
94
|
+
}
|
|
95
|
+
return done;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const skillName = name => typeof name === 'string' && /^[\w.-]+$/.test(name) && name !== '.' && name !== '..';
|
|
99
|
+
|
|
100
|
+
/** The library skills assigned to an agent, as named absolute SKILL.md paths that exist. */
|
|
101
|
+
export async function librarySkills(home, agent) {
|
|
102
|
+
const file = path.join(home, 'role-skills.json');
|
|
103
|
+
const text = await optional(file);
|
|
104
|
+
if (text === undefined) return [];
|
|
105
|
+
let roles;
|
|
106
|
+
try { roles = JSON.parse(text); }
|
|
107
|
+
catch (cause) { throw new AlpError('INVALID_LIBRARY', `${file}: ${cause.message}`, { cause }); }
|
|
108
|
+
if (!roles || typeof roles !== 'object' || Array.isArray(roles)) throw new AlpError('INVALID_LIBRARY', `${file}: expected an object of role names to skill lists`);
|
|
109
|
+
const names = roles[agent] ?? [];
|
|
110
|
+
if (!Array.isArray(names) || !names.every(skillName)) throw new AlpError('INVALID_LIBRARY', `${file}: ${agent} must list skill directory names`);
|
|
111
|
+
const skills = [];
|
|
112
|
+
for (const name of new Set(names)) {
|
|
113
|
+
const skillPath = path.join(home, 'skills', name, 'SKILL.md');
|
|
114
|
+
if (await optional(skillPath) !== undefined) skills.push({ name, path: skillPath });
|
|
115
|
+
}
|
|
116
|
+
return skills;
|
|
117
|
+
}
|
package/src/log.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
2
|
+
export const LEVELS: Readonly<Record<LogLevel, number>>;
|
|
3
|
+
export const REDACTED: string;
|
|
4
|
+
export const OMITTED: string;
|
|
5
|
+
export type LogFields = {
|
|
6
|
+
code?: string;
|
|
7
|
+
error?: unknown;
|
|
8
|
+
sessionId?: string;
|
|
9
|
+
rootId?: string;
|
|
10
|
+
assignmentId?: string;
|
|
11
|
+
clientMessageId?: string;
|
|
12
|
+
requestId?: string;
|
|
13
|
+
agent?: string;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
};
|
|
16
|
+
export type LogRecord = {
|
|
17
|
+
time: string;
|
|
18
|
+
level: LogLevel;
|
|
19
|
+
component: string;
|
|
20
|
+
code?: string;
|
|
21
|
+
msg: string;
|
|
22
|
+
error?: { name?: string; code?: string | number; message: string; stack?: string; cause?: unknown };
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
};
|
|
25
|
+
export type Logger = {
|
|
26
|
+
component: string;
|
|
27
|
+
debug(message: string, fields?: LogFields): void;
|
|
28
|
+
info(message: string, fields?: LogFields): void;
|
|
29
|
+
warn(message: string, fields?: LogFields): void;
|
|
30
|
+
error(message: string, fields?: LogFields): void;
|
|
31
|
+
child(extra?: LogFields, component?: string): Logger;
|
|
32
|
+
};
|
|
33
|
+
export function scrubText(text: unknown, options?: { env?: Record<string, string | undefined>; max?: number }): string;
|
|
34
|
+
export function describeError(error: unknown, options?: { env?: Record<string, string | undefined> }): { name?: string; code?: string | number; message: string; stack?: string; cause?: unknown };
|
|
35
|
+
export function redact(value: unknown, options?: { env?: Record<string, string | undefined> }): any;
|
|
36
|
+
export function createLogger(options: { component: string; level?: LogLevel; context?: LogFields; write?: (line: string) => void; now?: () => Date; env?: Record<string, string | undefined> }): Logger;
|
|
37
|
+
export function readLogTail(file: string, options?: { bytes?: number; env?: Record<string, string | undefined> }): Promise<Array<LogRecord | { legacy: string }>>;
|