@promptbook/cli 0.114.0-38 → 0.114.0-39
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/agents/default/.core/adam.book +33 -0
- package/apps/agents-server/src/utils/agentReferenceResolver/AgentReferenceResolutionIssue.ts +1 -69
- package/apps/agents-server/src/utils/agentReferenceResolver/extractAgentReferenceTokens.ts +1 -61
- package/apps/agents-server/src/utils/explicitFromCommitment.ts +1 -130
- package/apps/agents-server/src/utils/resolveInheritedAgentSource.ts +32 -817
- package/esm/index.es.js +1069 -31
- package/esm/index.es.js.map +1 -1
- package/esm/scripts/run-agent-messages/messages/createAgentRunnerSystemMessage.d.ts +2 -1
- package/esm/scripts/run-codex-prompts/common/resolveCoderAgent.d.ts +4 -0
- package/esm/scripts/run-codex-prompts/git/commitInitializedAgentBooks.d.ts +5 -0
- package/esm/src/book-2.0/agent-source/AgentReferenceResolutionIssue.d.ts +45 -0
- package/esm/src/book-2.0/agent-source/explicitFromCommitment.d.ts +62 -0
- package/esm/src/book-2.0/agent-source/extractAgentReferenceTokens.d.ts +32 -0
- package/esm/src/book-2.0/agent-source/resolveInheritedAgentSource.d.ts +73 -0
- package/esm/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.d.ts +1 -0
- package/esm/src/cli/cli-commands/common/LocalAgentBookCollection.d.ts +46 -0
- package/esm/src/cli/cli-commands/common/createLocalAgentReferenceResolver.d.ts +8 -0
- package/esm/src/cli/cli-commands/common/ensureAdamAgentBook.d.ts +12 -0
- package/esm/src/cli/cli-commands/common/resolveBundledAgentBookPath.d.ts +6 -0
- package/esm/src/cli/cli-commands/common/resolveLocalAgentSource.d.ts +19 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/book-2.0/agent-source/AgentReferenceResolutionIssue.ts +75 -0
- package/src/book-2.0/agent-source/explicitFromCommitment.ts +132 -0
- package/src/book-2.0/agent-source/extractAgentReferenceTokens.ts +65 -0
- package/src/book-2.0/agent-source/resolveInheritedAgentSource.ts +841 -0
- package/src/cli/cli-commands/coder/ensureCoderDeveloperAgentFile.ts +2 -28
- package/src/cli/cli-commands/coder/init.ts +3 -1
- package/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.ts +5 -0
- package/src/cli/cli-commands/coder/list.ts +5 -3
- package/src/cli/cli-commands/coder/printInitializationSummary.ts +6 -0
- package/src/cli/cli-commands/coder/run.ts +4 -1
- package/src/cli/cli-commands/coder/server.ts +2 -1
- package/src/cli/cli-commands/common/LocalAgentBookCollection.ts +220 -0
- package/src/cli/cli-commands/common/createLocalAgentReferenceResolver.ts +62 -0
- package/src/cli/cli-commands/common/ensureAdamAgentBook.ts +40 -0
- package/src/cli/cli-commands/common/resolveBundledAgentBookPath.ts +38 -0
- package/src/cli/cli-commands/common/resolveLocalAgentSource.ts +127 -0
- package/src/other/templates/getTemplatesPipelineCollection.ts +650 -928
- package/src/version.ts +2 -2
- package/src/versions.txt +1 -0
- package/umd/index.umd.js +1069 -31
- package/umd/index.umd.js.map +1 -1
- package/umd/scripts/run-agent-messages/messages/createAgentRunnerSystemMessage.d.ts +2 -1
- package/umd/scripts/run-codex-prompts/common/resolveCoderAgent.d.ts +4 -0
- package/umd/scripts/run-codex-prompts/git/commitInitializedAgentBooks.d.ts +5 -0
- package/umd/src/book-2.0/agent-source/AgentReferenceResolutionIssue.d.ts +45 -0
- package/umd/src/book-2.0/agent-source/explicitFromCommitment.d.ts +62 -0
- package/umd/src/book-2.0/agent-source/extractAgentReferenceTokens.d.ts +32 -0
- package/umd/src/book-2.0/agent-source/resolveInheritedAgentSource.d.ts +73 -0
- package/umd/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.d.ts +1 -0
- package/umd/src/cli/cli-commands/common/LocalAgentBookCollection.d.ts +46 -0
- package/umd/src/cli/cli-commands/common/createLocalAgentReferenceResolver.d.ts +8 -0
- package/umd/src/cli/cli-commands/common/ensureAdamAgentBook.d.ts +12 -0
- package/umd/src/cli/cli-commands/common/resolveBundledAgentBookPath.d.ts +6 -0
- package/umd/src/cli/cli-commands/common/resolveLocalAgentSource.d.ts +19 -0
- package/umd/src/version.d.ts +1 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { copyFile, stat } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import {
|
|
4
|
-
import { NotAllowed } from '../../../errors/NotAllowed';
|
|
3
|
+
import { resolveBundledAgentBookPath } from '../common/resolveBundledAgentBookPath';
|
|
5
4
|
import type { InitializationStatus } from './boilerplateTemplates';
|
|
6
5
|
|
|
7
6
|
/**
|
|
@@ -36,35 +35,10 @@ export async function ensureCoderDeveloperAgentFile(projectPath: string): Promis
|
|
|
36
35
|
return 'unchanged';
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
await copyFile(await
|
|
38
|
+
await copyFile(await resolveBundledAgentBookPath(DEFAULT_CODER_DEVELOPER_AGENT_SOURCE_FILE_PATH), absoluteFilePath);
|
|
40
39
|
return 'created';
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
/**
|
|
44
|
-
* Resolves the bundled developer agent from a source checkout or generated CLI package.
|
|
45
|
-
*/
|
|
46
|
-
async function resolveDefaultCoderDeveloperAgentFilePath(): Promise<string> {
|
|
47
|
-
const candidates = [
|
|
48
|
-
join(__dirname, '..', DEFAULT_CODER_DEVELOPER_AGENT_SOURCE_FILE_PATH),
|
|
49
|
-
join(__dirname, '..', '..', '..', '..', DEFAULT_CODER_DEVELOPER_AGENT_SOURCE_FILE_PATH),
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
for (const candidate of candidates) {
|
|
53
|
-
if (await isExistingFile(candidate)) {
|
|
54
|
-
return candidate;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
throw new NotAllowed(
|
|
59
|
-
spaceTrim(`
|
|
60
|
-
Cannot find the bundled Promptbook developer agent.
|
|
61
|
-
|
|
62
|
-
Checked:
|
|
63
|
-
${candidates.map((candidate) => `- \`${candidate}\``).join('\n')}
|
|
64
|
-
`),
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
42
|
/**
|
|
69
43
|
* Checks whether a path exists and is a file.
|
|
70
44
|
*/
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
} from 'commander';
|
|
4
4
|
import { spaceTrim } from 'spacetrim';
|
|
5
5
|
import type { $side_effect } from '../../../utils/organization/$side_effect';
|
|
6
|
+
import { ADAM_AGENT_BOOK_RELATIVE_PATH } from '../common/ensureAdamAgentBook';
|
|
6
7
|
import type { CoderGitSyncCliOptions } from '../common/coderGitSyncCliOptions';
|
|
7
8
|
import {
|
|
8
9
|
addCoderGitSyncOptions,
|
|
@@ -22,7 +23,7 @@ import type { PromptRunnerHarnessName } from '../common/promptRunnerCliOptions';
|
|
|
22
23
|
import { AGENTS_FILE_PATH } from './agentsFile';
|
|
23
24
|
import { DEFAULT_BOILERPLATE_COUNT } from './boilerplateCount';
|
|
24
25
|
import { getDefaultCoderProjectPromptTemplateDefinitions, PROMPTS_DIRECTORY_PATH } from './boilerplateTemplates';
|
|
25
|
-
import { CODER_DEVELOPER_AGENT_FILE_PATH } from './ensureCoderDeveloperAgentFile';
|
|
26
|
+
import { CODER_AGENTS_DIRECTORY_PATH, CODER_DEVELOPER_AGENT_FILE_PATH } from './ensureCoderDeveloperAgentFile';
|
|
26
27
|
import { isDirectoryEmpty } from './ensureDirectory';
|
|
27
28
|
import { formatDisplayPath } from './formatDisplayPath';
|
|
28
29
|
import { generatePromptBoilerplate } from './generate-boilerplates';
|
|
@@ -61,6 +62,7 @@ export function $initializeCoderInitCommand(program: Program): $side_effect {
|
|
|
61
62
|
- prompts/done/
|
|
62
63
|
${block(listDefaultCoderProjectPromptTemplateDisplayPaths())}
|
|
63
64
|
- ${CODER_DEVELOPER_AGENT_FILE_PATH}
|
|
65
|
+
- ${CODER_AGENTS_DIRECTORY_PATH}/${ADAM_AGENT_BOOK_RELATIVE_PATH}
|
|
64
66
|
- ${AGENTS_FILE_PATH}
|
|
65
67
|
- .gitignore with local artifacts from every supported harness
|
|
66
68
|
- package.json
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { ensureAdamAgentBook } from '../common/ensureAdamAgentBook';
|
|
1
3
|
import type { InitializationStatus } from './boilerplateTemplates';
|
|
2
4
|
import {
|
|
3
5
|
PROMPTS_DIRECTORY_PATH,
|
|
@@ -24,6 +26,7 @@ export type CoderInitializationSummary = {
|
|
|
24
26
|
readonly promptsDoneDirectoryStatus: InitializationStatus;
|
|
25
27
|
readonly promptsTemplatesDirectoryStatus: InitializationStatus;
|
|
26
28
|
readonly agentsDirectoryStatus: InitializationStatus;
|
|
29
|
+
readonly adamAgentFileStatus: InitializationStatus;
|
|
27
30
|
readonly envFileStatus: InitializationStatus;
|
|
28
31
|
readonly gitignoreFileStatus: InitializationStatus;
|
|
29
32
|
readonly packageJsonFileStatus: InitializationStatus;
|
|
@@ -46,6 +49,7 @@ export async function initializeCoderProjectConfiguration(projectPath: string):
|
|
|
46
49
|
const promptsDoneDirectoryStatus = await ensureDirectory(projectPath, PROMPTS_DONE_DIRECTORY_PATH);
|
|
47
50
|
const promptsTemplatesDirectoryStatus = await ensureDirectory(projectPath, PROMPTS_TEMPLATES_DIRECTORY_PATH);
|
|
48
51
|
const agentsDirectoryStatus = await ensureDirectory(projectPath, CODER_AGENTS_DIRECTORY_PATH);
|
|
52
|
+
const adamAgentFileStatus = await ensureAdamAgentBook(join(projectPath, CODER_AGENTS_DIRECTORY_PATH));
|
|
49
53
|
const { envFileStatus, initializedEnvVariableNames } = await ensureCoderEnvFile(projectPath);
|
|
50
54
|
const gitignoreFileStatus = await ensureCoderGitignoreFile(projectPath);
|
|
51
55
|
const { status: packageJsonFileStatus, addedEntryKeys: addedPackageJsonScriptNames } =
|
|
@@ -61,6 +65,7 @@ export async function initializeCoderProjectConfiguration(projectPath: string):
|
|
|
61
65
|
promptsDoneDirectoryStatus,
|
|
62
66
|
promptsTemplatesDirectoryStatus,
|
|
63
67
|
agentsDirectoryStatus,
|
|
68
|
+
adamAgentFileStatus,
|
|
64
69
|
envFileStatus,
|
|
65
70
|
gitignoreFileStatus,
|
|
66
71
|
packageJsonFileStatus,
|
|
@@ -52,7 +52,8 @@ export function $initializeCoderListCommand(program: Program): $side_effect {
|
|
|
52
52
|
readonly priority?: number;
|
|
53
53
|
readonly minPriority?: number;
|
|
54
54
|
readonly maxPriority?: number;
|
|
55
|
-
} & PromptRunnerSelectionCliOptions &
|
|
55
|
+
} & PromptRunnerSelectionCliOptions &
|
|
56
|
+
CoderAgentCliOptions;
|
|
56
57
|
const runnerOptions = normalizePromptRunnerSelectionCliOptions(
|
|
57
58
|
cliOptions as PromptRunnerSelectionCliOptions,
|
|
58
59
|
{ isAgentRequired: false },
|
|
@@ -60,8 +61,9 @@ export function $initializeCoderListCommand(program: Program): $side_effect {
|
|
|
60
61
|
const resolvedCoderAgentBook =
|
|
61
62
|
agent === undefined
|
|
62
63
|
? undefined
|
|
63
|
-
: await (
|
|
64
|
-
|
|
64
|
+
: await (
|
|
65
|
+
await import('../../../../scripts/run-codex-prompts/common/resolveCoderAgent')
|
|
66
|
+
).resolveCoderAgentBook(agent, process.cwd());
|
|
65
67
|
const promptRunnerIdentity =
|
|
66
68
|
runnerOptions.agentName === undefined &&
|
|
67
69
|
runnerOptions.model === undefined &&
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import colors from 'colors';
|
|
2
|
+
import { ADAM_AGENT_BOOK_RELATIVE_PATH } from '../common/ensureAdamAgentBook';
|
|
3
|
+
import { CODER_AGENTS_DIRECTORY_PATH } from './ensureCoderDeveloperAgentFile';
|
|
2
4
|
import type { InitializationStatus } from './boilerplateTemplates';
|
|
3
5
|
import type { CoderReferencedArtifactStatus } from './coderReferencedArtifacts';
|
|
4
6
|
import { formatDisplayPath } from './formatDisplayPath';
|
|
@@ -15,6 +17,10 @@ export function printInitializationSummary(summary: CoderInitializationSummary):
|
|
|
15
17
|
printInitializationStatusLine('prompts/done/', summary.promptsDoneDirectoryStatus);
|
|
16
18
|
printInitializationStatusLine('prompts/templates/', summary.promptsTemplatesDirectoryStatus);
|
|
17
19
|
printInitializationStatusLine('agents/', summary.agentsDirectoryStatus);
|
|
20
|
+
printInitializationStatusLine(
|
|
21
|
+
`${CODER_AGENTS_DIRECTORY_PATH}/${ADAM_AGENT_BOOK_RELATIVE_PATH}`,
|
|
22
|
+
summary.adamAgentFileStatus,
|
|
23
|
+
);
|
|
18
24
|
printInitializationStatusLine('.env', summary.envFileStatus);
|
|
19
25
|
printInitializationStatusLine('.gitignore', summary.gitignoreFileStatus);
|
|
20
26
|
printInitializationStatusLine('package.json', summary.packageJsonFileStatus);
|
|
@@ -54,6 +54,8 @@ export function $initializeCoderRunCommand(program: Program): $side_effect {
|
|
|
54
54
|
${block(QUESTIONS_DESCRIPTION)}
|
|
55
55
|
|
|
56
56
|
Features:
|
|
57
|
+
- Resolves FROM, IMPORT and TEAM by agent name, book-relative path, project path or URL
|
|
58
|
+
- Creates a missing .core/adam.book beside the selected agent for default inheritance
|
|
57
59
|
- Automatically stages and commits changes with agent identity unless --no-commit is used
|
|
58
60
|
- Commits only the prompt file and the files the coding agent has changed, leaving unrelated changes alone
|
|
59
61
|
- Refuses to start on a dirty working tree unless --git-changes says what should happen with the changes
|
|
@@ -194,7 +196,8 @@ export function $initializeCoderRunCommand(program: Program): $side_effect {
|
|
|
194
196
|
readonly auto: boolean;
|
|
195
197
|
readonly autoMigrate: boolean;
|
|
196
198
|
readonly allowDestructiveAutoMigrate: boolean;
|
|
197
|
-
} & PromptRunnerCliOptions &
|
|
199
|
+
} & PromptRunnerCliOptions &
|
|
200
|
+
CoderAgentCliOptions;
|
|
198
201
|
|
|
199
202
|
const configuredTestCommand = normalizeCommandOptionValue(test);
|
|
200
203
|
const testCommand = configuredTestCommand ?? (testBefore === 'no' ? undefined : DEFAULT_CODER_TEST_COMMAND);
|
|
@@ -166,7 +166,8 @@ export function $initializeCoderServerCommand(program: Program): $side_effect {
|
|
|
166
166
|
readonly auto: boolean;
|
|
167
167
|
readonly autoMigrate: boolean;
|
|
168
168
|
readonly allowDestructiveAutoMigrate: boolean;
|
|
169
|
-
} & PromptRunnerCliOptions &
|
|
169
|
+
} & PromptRunnerCliOptions &
|
|
170
|
+
CoderAgentCliOptions;
|
|
170
171
|
|
|
171
172
|
const port = parseCoderServerPort(rawPort);
|
|
172
173
|
const testCommand = normalizeCommandOptionValue(test);
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { readFile, readdir, realpath } from 'fs/promises';
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import { spaceTrim } from 'spacetrim';
|
|
5
|
+
import { normalizeAgentName } from '../../../book-2.0/agent-source/normalizeAgentName';
|
|
6
|
+
import { parseAgentSource } from '../../../book-2.0/agent-source/parseAgentSource';
|
|
7
|
+
import type { string_book } from '../../../book-2.0/agent-source/string_book';
|
|
8
|
+
import type { TeammateProfile } from '../../../book-2.0/agent-source/TeammateProfileResolver';
|
|
9
|
+
import { NotFoundError } from '../../../errors/NotFoundError';
|
|
10
|
+
import { ParseError } from '../../../errors/ParseError';
|
|
11
|
+
import { isValidAgentUrl } from '../../../utils/validators/url/isValidAgentUrl';
|
|
12
|
+
import { ADAM_AGENT_BOOK_RELATIVE_PATH, ensureAdamAgentBook } from './ensureAdamAgentBook';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A book's source and identity, retaining its location for nested relative references.
|
|
16
|
+
*
|
|
17
|
+
* @private internal type of CLI agent resolution
|
|
18
|
+
*/
|
|
19
|
+
export type LocalAgentBook = {
|
|
20
|
+
readonly url: string;
|
|
21
|
+
readonly filePath?: string;
|
|
22
|
+
readonly source: string_book;
|
|
23
|
+
readonly profile: TeammateProfile;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Internal URLs allow the shared Book commitments to identify local books without an HTTP server.
|
|
28
|
+
* These identifiers are always intercepted by this collection and are never fetched.
|
|
29
|
+
*/
|
|
30
|
+
const LOCAL_AGENT_URL_PREFIX = 'https://local-agent.promptbook/';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Directories which cannot contain project-owned agent definitions.
|
|
34
|
+
*/
|
|
35
|
+
const IGNORED_DIRECTORY_NAMES = new Set(['.git', 'node_modules']);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Reads repository books and indexes their first-line names for CLI reference resolution.
|
|
39
|
+
*
|
|
40
|
+
* @private internal utility of CLI agent resolution
|
|
41
|
+
*/
|
|
42
|
+
export class LocalAgentBookCollection {
|
|
43
|
+
private readonly booksByUrl = new Map<string, LocalAgentBook>();
|
|
44
|
+
private readonly booksByName = new Map<string, LocalAgentBook[]>();
|
|
45
|
+
/** Stable default ancestor selected from the initial repository scan. */
|
|
46
|
+
private adamAgentUrl: string | undefined;
|
|
47
|
+
/** Files initialized by this collection, for the coder's normal commit handling. */
|
|
48
|
+
public readonly createdAgentBookPaths: string[] = [];
|
|
49
|
+
|
|
50
|
+
/** Creates a collection rooted at the primary book's directory. */
|
|
51
|
+
public constructor(private readonly agentDirectoryPath: string, private readonly currentWorkingDirectory: string) {}
|
|
52
|
+
|
|
53
|
+
/** Discovers nested books, including the hidden `.core` directory. */
|
|
54
|
+
public async initialize(): Promise<void> {
|
|
55
|
+
await this.readDirectory(this.agentDirectoryPath);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Identifies the default ancestor without creating an unused book for FROM null/void. */
|
|
59
|
+
public getAdamAgentUrl(): string {
|
|
60
|
+
this.adamAgentUrl ??= this.booksByName.has(normalizeAgentName('Adam'))
|
|
61
|
+
? this.findByName('Adam').url
|
|
62
|
+
: createLocalAgentBookUrl(join(this.agentDirectoryPath, ADAM_AGENT_BOOK_RELATIVE_PATH));
|
|
63
|
+
return this.adamAgentUrl;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Resolves a unique first-line name, refusing ambiguous matches. */
|
|
67
|
+
public findByName(name: string): LocalAgentBook {
|
|
68
|
+
const matches = this.booksByName.get(normalizeAgentName(name)) || [];
|
|
69
|
+
if (matches.length === 1) {
|
|
70
|
+
return matches[0]!;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (matches.length > 1) {
|
|
74
|
+
throw new ParseError(
|
|
75
|
+
spaceTrim(
|
|
76
|
+
(block) => `
|
|
77
|
+
Agent reference \`${name}\` is ambiguous. Use an explicit book path.
|
|
78
|
+
|
|
79
|
+
Matching books:
|
|
80
|
+
${block(matches.map((book) => `- \`${book.filePath || book.url}\``).join('\n'))}
|
|
81
|
+
`,
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new NotFoundError(
|
|
87
|
+
spaceTrim(`
|
|
88
|
+
Agent \`${name}\` was not found in \`${this.agentDirectoryPath}\` or its subdirectories.
|
|
89
|
+
|
|
90
|
+
References use the agent's **first-line name**. You can also use an explicit \`.book\` path or URL.
|
|
91
|
+
`),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Resolves a name, a path relative to its declaring book, or a path relative to the CLI cwd. */
|
|
96
|
+
public async resolveReference(reference: string, declaringBook: LocalAgentBook): Promise<string> {
|
|
97
|
+
const isRemoteReference = Boolean(isValidAgentUrl(reference));
|
|
98
|
+
if (isRemoteReference) {
|
|
99
|
+
return reference;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const isBookPath = /\.book$/i.test(reference) || isAbsolute(reference);
|
|
103
|
+
if (!isBookPath) {
|
|
104
|
+
if (normalizeAgentName(reference) === normalizeAgentName('Adam')) {
|
|
105
|
+
return this.getAdamAgentUrl();
|
|
106
|
+
}
|
|
107
|
+
return this.findByName(reference).url;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const isRelativeToBook = /^\.{1,2}[\\/]/.test(reference);
|
|
111
|
+
if (!declaringBook.filePath && isRelativeToBook) {
|
|
112
|
+
return new URL(reference.replaceAll('\\', '/'), declaringBook.url).href;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const directoryPath =
|
|
116
|
+
isRelativeToBook && declaringBook.filePath ? dirname(declaringBook.filePath) : this.currentWorkingDirectory;
|
|
117
|
+
return (await this.readBook(resolve(directoryPath, reference))).url;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Reads a local book once, canonicalizing symlinks so aliases cannot bypass cycle detection. */
|
|
121
|
+
public async readBook(filePath: string): Promise<LocalAgentBook> {
|
|
122
|
+
try {
|
|
123
|
+
const canonicalFilePath = await realpath(filePath);
|
|
124
|
+
const url = createLocalAgentBookUrl(canonicalFilePath);
|
|
125
|
+
const existingBook = this.booksByUrl.get(url);
|
|
126
|
+
if (existingBook) {
|
|
127
|
+
return existingBook;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return this.registerBook(url, await readFile(canonicalFilePath, 'utf-8'), canonicalFilePath);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (['ENOENT', 'EISDIR', 'ENOTDIR'].includes((error as NodeJS.ErrnoException).code || '')) {
|
|
133
|
+
throw new NotFoundError(spaceTrim(`Agent book \`${filePath}\` was not found or is not a file.`));
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Loads a referenced remote book, while keeping local identifiers entirely inside the collection. */
|
|
140
|
+
public async getBook(url: string): Promise<LocalAgentBook> {
|
|
141
|
+
const existingBook = this.booksByUrl.get(url);
|
|
142
|
+
if (existingBook) {
|
|
143
|
+
return existingBook;
|
|
144
|
+
}
|
|
145
|
+
if (url === this.getAdamAgentUrl()) {
|
|
146
|
+
const adamBookPath = join(this.agentDirectoryPath, ADAM_AGENT_BOOK_RELATIVE_PATH);
|
|
147
|
+
if ((await ensureAdamAgentBook(this.agentDirectoryPath)) === 'created') {
|
|
148
|
+
this.createdAgentBookPaths.push(adamBookPath);
|
|
149
|
+
}
|
|
150
|
+
const adamBook = await this.readBook(adamBookPath);
|
|
151
|
+
this.booksByUrl.set(url, adamBook);
|
|
152
|
+
return adamBook;
|
|
153
|
+
}
|
|
154
|
+
if (url.startsWith(LOCAL_AGENT_URL_PREFIX)) {
|
|
155
|
+
throw new NotFoundError(spaceTrim(`Local agent book \`${url}\` is not registered.`));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const bookUrl = new URL(url);
|
|
159
|
+
if (!/\.(book|md)$/i.test(bookUrl.pathname) && !bookUrl.pathname.endsWith('/api/book')) {
|
|
160
|
+
bookUrl.pathname = bookUrl.pathname.replace(/\/$/, '') + '/api/book';
|
|
161
|
+
}
|
|
162
|
+
const response = await fetch(bookUrl.href);
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
throw new NotFoundError(
|
|
165
|
+
spaceTrim(`Cannot load agent book \`${url}\`: **${response.status} ${response.statusText}**.`),
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
let source: unknown;
|
|
169
|
+
if (response.headers.get('content-type')?.includes('application/json')) {
|
|
170
|
+
const payload: unknown = await response.json();
|
|
171
|
+
source = typeof payload === 'string' ? payload : (payload as { source?: unknown } | null)?.source;
|
|
172
|
+
} else {
|
|
173
|
+
source = await response.text();
|
|
174
|
+
}
|
|
175
|
+
if (typeof source !== 'string') {
|
|
176
|
+
throw new ParseError(spaceTrim(`Agent book \`${url}\` did not return a text source.`));
|
|
177
|
+
}
|
|
178
|
+
return this.registerBook(url, source);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Adds a book under its normalized first-line name and preserves the display name for TEAM. */
|
|
182
|
+
private registerBook(url: string, source: string, filePath?: string): LocalAgentBook {
|
|
183
|
+
const agentSource = source as string_book;
|
|
184
|
+
const parsedSource = parseAgentSource(agentSource);
|
|
185
|
+
const book: LocalAgentBook = {
|
|
186
|
+
url,
|
|
187
|
+
filePath,
|
|
188
|
+
source: agentSource,
|
|
189
|
+
profile: {
|
|
190
|
+
agentName: parsedSource.meta.fullname || parsedSource.agentName,
|
|
191
|
+
personaDescription: parsedSource.personaDescription,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
this.booksByUrl.set(url, book);
|
|
195
|
+
// Remote titles must not shadow repository-local names.
|
|
196
|
+
if (filePath) {
|
|
197
|
+
const agentName = normalizeAgentName(parsedSource.agentName);
|
|
198
|
+
this.booksByName.set(agentName, [...(this.booksByName.get(agentName) || []), book]);
|
|
199
|
+
}
|
|
200
|
+
return book;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Walks real directories in deterministic order without following directory symlink loops. */
|
|
204
|
+
private async readDirectory(directoryPath: string): Promise<void> {
|
|
205
|
+
const entries = await readdir(directoryPath, { withFileTypes: true });
|
|
206
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
207
|
+
const entryPath = join(directoryPath, entry.name);
|
|
208
|
+
if (entry.isDirectory() && !IGNORED_DIRECTORY_NAMES.has(entry.name)) {
|
|
209
|
+
await this.readDirectory(entryPath);
|
|
210
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith('.book')) {
|
|
211
|
+
await this.readBook(entryPath);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Creates an internal URL for a local path without requiring that the file already exists. */
|
|
218
|
+
function createLocalAgentBookUrl(filePath: string): string {
|
|
219
|
+
return LOCAL_AGENT_URL_PREFIX + encodeURIComponent(pathToFileURL(filePath).href);
|
|
220
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { spaceTrim } from 'spacetrim';
|
|
2
|
+
import type { AgentReferenceResolver } from '../../../book-2.0/agent-source/AgentReferenceResolver';
|
|
3
|
+
import { extractAgentReferenceTokens } from '../../../book-2.0/agent-source/extractAgentReferenceTokens';
|
|
4
|
+
import {
|
|
5
|
+
createPseudoAgentUrl,
|
|
6
|
+
isPseudoAgentAllowedInCommitment,
|
|
7
|
+
resolvePseudoAgentKindFromReference,
|
|
8
|
+
VOID_PSEUDO_AGENT_REFERENCE,
|
|
9
|
+
} from '../../../book-2.0/agent-source/pseudoAgentReferences';
|
|
10
|
+
import type { BookCommitment } from '../../../commitments/_base/BookCommitment';
|
|
11
|
+
import { ParseError } from '../../../errors/ParseError';
|
|
12
|
+
import type { LocalAgentBook, LocalAgentBookCollection } from './LocalAgentBookCollection';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a resolver scoped to the book which declares each reference.
|
|
16
|
+
*
|
|
17
|
+
* @private internal utility of CLI agent resolution
|
|
18
|
+
*/
|
|
19
|
+
export function createLocalAgentReferenceResolver(
|
|
20
|
+
collection: LocalAgentBookCollection,
|
|
21
|
+
declaringBook: LocalAgentBook,
|
|
22
|
+
): AgentReferenceResolver {
|
|
23
|
+
/** Resolves pseudo-agents using the same aliases and restrictions as the Agent Server. */
|
|
24
|
+
async function resolveReference(commitmentType: BookCommitment, reference: string): Promise<string> {
|
|
25
|
+
const pseudoAgentKind = resolvePseudoAgentKindFromReference(reference);
|
|
26
|
+
if (!pseudoAgentKind) {
|
|
27
|
+
return collection.resolveReference(reference, declaringBook);
|
|
28
|
+
}
|
|
29
|
+
if (!isPseudoAgentAllowedInCommitment(pseudoAgentKind, commitmentType)) {
|
|
30
|
+
throw new ParseError(spaceTrim(`Pseudo-agent \`${reference}\` cannot be used in \`${commitmentType}\`.`));
|
|
31
|
+
}
|
|
32
|
+
if (pseudoAgentKind === 'VOID' && commitmentType === 'FROM') {
|
|
33
|
+
return VOID_PSEUDO_AGENT_REFERENCE;
|
|
34
|
+
}
|
|
35
|
+
if (pseudoAgentKind === 'VOID' && (commitmentType === 'IMPORT' || commitmentType === 'IMPORTS')) {
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
return createPseudoAgentUrl(pseudoAgentKind);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
resolveCommitmentContent: async (commitmentType, content) => {
|
|
43
|
+
const tokens = extractAgentReferenceTokens(content);
|
|
44
|
+
if (tokens.length === 0) {
|
|
45
|
+
if (commitmentType !== 'TEAM' && content.trim()) {
|
|
46
|
+
return resolveReference(commitmentType, content.trim());
|
|
47
|
+
}
|
|
48
|
+
return content;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const parts: string[] = [];
|
|
52
|
+
let previousEnd = 0;
|
|
53
|
+
for (const token of tokens) {
|
|
54
|
+
parts.push(content.slice(previousEnd, token.index));
|
|
55
|
+
parts.push(await resolveReference(commitmentType, token.reference));
|
|
56
|
+
previousEnd = token.index + token.length;
|
|
57
|
+
}
|
|
58
|
+
parts.push(content.slice(previousEnd));
|
|
59
|
+
return parts.join('');
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { constants } from 'fs';
|
|
2
|
+
import { copyFile, mkdir, stat } from 'fs/promises';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { resolveBundledAgentBookPath } from './resolveBundledAgentBookPath';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Adam's location relative to a directory of local agents.
|
|
8
|
+
*
|
|
9
|
+
* @private internal constant of CLI agent initialization
|
|
10
|
+
*/
|
|
11
|
+
export const ADAM_AGENT_BOOK_RELATIVE_PATH = '.core/adam.book';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates the same Adam book used by the Agent Server without overwriting a project-owned book.
|
|
15
|
+
*
|
|
16
|
+
* @private internal utility of CLI agent initialization
|
|
17
|
+
*/
|
|
18
|
+
export async function ensureAdamAgentBook(agentDirectoryPath: string): Promise<'created' | 'unchanged'> {
|
|
19
|
+
const adamBookPath = join(agentDirectoryPath, ADAM_AGENT_BOOK_RELATIVE_PATH);
|
|
20
|
+
if (
|
|
21
|
+
await stat(adamBookPath).then(
|
|
22
|
+
(entry) => entry.isFile(),
|
|
23
|
+
() => false,
|
|
24
|
+
)
|
|
25
|
+
) {
|
|
26
|
+
return 'unchanged';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const bundledAdamBookPath = await resolveBundledAgentBookPath('agents/default/.core/adam.book');
|
|
30
|
+
await mkdir(dirname(adamBookPath), { recursive: true });
|
|
31
|
+
try {
|
|
32
|
+
await copyFile(bundledAdamBookPath, adamBookPath, constants.COPYFILE_EXCL);
|
|
33
|
+
return 'created';
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
|
|
36
|
+
return 'unchanged';
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { stat } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { spaceTrim } from 'spacetrim';
|
|
4
|
+
import { NotFoundError } from '../../../errors/NotFoundError';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Locates a bundled agent in either the source checkout or the generated CLI package.
|
|
8
|
+
*
|
|
9
|
+
* @private internal utility of CLI agent initialization
|
|
10
|
+
*/
|
|
11
|
+
export async function resolveBundledAgentBookPath(relativeFilePath: string): Promise<string> {
|
|
12
|
+
const candidates = [
|
|
13
|
+
join(__dirname, '..', relativeFilePath),
|
|
14
|
+
join(__dirname, '..', '..', '..', '..', relativeFilePath),
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
for (const candidate of candidates) {
|
|
18
|
+
if (
|
|
19
|
+
await stat(candidate).then(
|
|
20
|
+
(entry) => entry.isFile(),
|
|
21
|
+
() => false,
|
|
22
|
+
)
|
|
23
|
+
) {
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw new NotFoundError(
|
|
29
|
+
spaceTrim(
|
|
30
|
+
(block) => `
|
|
31
|
+
Cannot find the bundled agent book \`${relativeFilePath}\`.
|
|
32
|
+
|
|
33
|
+
Checked:
|
|
34
|
+
${block(candidates.map((candidate) => `- \`${candidate}\``).join('\n'))}
|
|
35
|
+
`,
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { dirname } from 'path';
|
|
2
|
+
import { spaceTrim } from 'spacetrim';
|
|
3
|
+
import type { AgentReferenceResolver } from '../../../book-2.0/agent-source/AgentReferenceResolver';
|
|
4
|
+
import { getEffectiveExplicitFromCommitment } from '../../../book-2.0/agent-source/explicitFromCommitment';
|
|
5
|
+
import { parseAgentSourceWithCommitments } from '../../../book-2.0/agent-source/parseAgentSourceWithCommitments';
|
|
6
|
+
import { parseAgentSource } from '../../../book-2.0/agent-source/parseAgentSource';
|
|
7
|
+
import { isPseudoAgentUrl } from '../../../book-2.0/agent-source/pseudoAgentReferences';
|
|
8
|
+
import { resolveInheritedAgentSource } from '../../../book-2.0/agent-source/resolveInheritedAgentSource';
|
|
9
|
+
import type { string_book } from '../../../book-2.0/agent-source/string_book';
|
|
10
|
+
import { DEFAULT_MAX_RECURSION } from '../../../config';
|
|
11
|
+
import { ParseError } from '../../../errors/ParseError';
|
|
12
|
+
import { createLocalAgentReferenceResolver } from './createLocalAgentReferenceResolver';
|
|
13
|
+
import { LocalAgentBookCollection, type LocalAgentBook } from './LocalAgentBookCollection';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolved source together with the profile resolver used when compiling TEAM commitments.
|
|
17
|
+
*
|
|
18
|
+
* @private internal type of CLI agent resolution
|
|
19
|
+
*/
|
|
20
|
+
export type ResolvedLocalAgentSource = {
|
|
21
|
+
readonly agentSource: string_book;
|
|
22
|
+
readonly agentReferenceResolver: AgentReferenceResolver;
|
|
23
|
+
/** Newly created books which the coder may include in its initialization commit. */
|
|
24
|
+
readonly createdAgentBookPaths: ReadonlyArray<string>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Applies the shared Agent Server inheritance rules to a repository's local and remote books.
|
|
29
|
+
*
|
|
30
|
+
* @private internal utility of CLI agent resolution
|
|
31
|
+
*/
|
|
32
|
+
export async function resolveLocalAgentSource(
|
|
33
|
+
agentBookPath: string,
|
|
34
|
+
currentWorkingDirectory: string,
|
|
35
|
+
): Promise<ResolvedLocalAgentSource> {
|
|
36
|
+
const collection = new LocalAgentBookCollection(dirname(agentBookPath), currentWorkingDirectory);
|
|
37
|
+
await collection.initialize();
|
|
38
|
+
const primaryBook = await collection.readBook(agentBookPath);
|
|
39
|
+
const adamAgentUrl = collection.getAdamAgentUrl();
|
|
40
|
+
|
|
41
|
+
/** Resolves each dependency with its own location and the current inheritance stack. */
|
|
42
|
+
async function resolveBook(
|
|
43
|
+
book: LocalAgentBook,
|
|
44
|
+
inheritancePath: ReadonlyArray<string> = [],
|
|
45
|
+
): Promise<string_book> {
|
|
46
|
+
if (inheritancePath.length > DEFAULT_MAX_RECURSION) {
|
|
47
|
+
throw new ParseError(
|
|
48
|
+
spaceTrim(
|
|
49
|
+
`Agent inheritance exceeds **${DEFAULT_MAX_RECURSION}** levels at \`${
|
|
50
|
+
book.filePath || book.url
|
|
51
|
+
}\`.`,
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const agentReferenceResolver = createLocalAgentReferenceResolver(collection, book);
|
|
56
|
+
const source = await resolveBookCommitmentReferences(book.source, agentReferenceResolver);
|
|
57
|
+
return resolveInheritedAgentSource(source, {
|
|
58
|
+
adamAgentUrl,
|
|
59
|
+
currentAgentUrl: book.url,
|
|
60
|
+
inheritancePath,
|
|
61
|
+
agentSourceImporter: async (url, context) =>
|
|
62
|
+
resolveBook(await collection.getBook(url), context.importAgentOptions.inheritancePath),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
agentSource: await resolveBook(primaryBook),
|
|
68
|
+
agentReferenceResolver: {
|
|
69
|
+
...createLocalAgentReferenceResolver(collection, primaryBook),
|
|
70
|
+
resolveTeammateProfile: async (url) => {
|
|
71
|
+
if (isPseudoAgentUrl(url)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const book = await collection.getBook(url);
|
|
75
|
+
const parsedSource = parseAgentSource(await resolveBook(book));
|
|
76
|
+
return {
|
|
77
|
+
agentName: book.profile.agentName,
|
|
78
|
+
personaDescription: parsedSource.personaDescription,
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
createdAgentBookPaths: collection.createdAgentBookPaths,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Resolves reference blocks before embedding dependencies so inherited TEAM paths keep their original directory.
|
|
88
|
+
* Parser-provided locations keep examples, unknown commitments and ordinary prose untouched.
|
|
89
|
+
*/
|
|
90
|
+
async function resolveBookCommitmentReferences(
|
|
91
|
+
source: string_book,
|
|
92
|
+
resolver: AgentReferenceResolver,
|
|
93
|
+
): Promise<string_book> {
|
|
94
|
+
const parsedSource = parseAgentSourceWithCommitments(source);
|
|
95
|
+
const lines = source.split(/\r?\n/);
|
|
96
|
+
const commitmentStartLines = [...parsedSource.commitments, ...(parsedSource.unknownCommitments || [])]
|
|
97
|
+
.map((commitment) => commitment.lineNumber - 1)
|
|
98
|
+
.sort((left, right) => left - right);
|
|
99
|
+
const effectiveFrom = getEffectiveExplicitFromCommitment(source);
|
|
100
|
+
|
|
101
|
+
for (const commitment of [...parsedSource.commitments].reverse()) {
|
|
102
|
+
if (!['FROM', 'IMPORT', 'IMPORTS', 'TEAM'].includes(commitment.type)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const startLine = commitment.lineNumber - 1;
|
|
106
|
+
if (commitment.type === 'FROM' && effectiveFrom?.lineIndex !== startLine) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
let endLine = commitmentStartLines.find((lineNumber) => lineNumber > startLine) ?? lines.length;
|
|
110
|
+
// A horizontal rule also terminates a commitment before the next registered keyword.
|
|
111
|
+
for (let lineIndex = startLine + 1; lineIndex < endLine; lineIndex++) {
|
|
112
|
+
if (/^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/.test(lines[lineIndex] || '')) {
|
|
113
|
+
endLine = lineIndex;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const content = await resolver.resolveCommitmentContent(commitment.type, commitment.content);
|
|
118
|
+
const commitmentType = commitment.type === 'IMPORTS' ? 'IMPORT' : commitment.type;
|
|
119
|
+
const replacement = content.trim()
|
|
120
|
+
? `${commitmentType} ${content}\n`
|
|
121
|
+
: commitmentType === 'FROM'
|
|
122
|
+
? 'FROM @Null\n'
|
|
123
|
+
: '';
|
|
124
|
+
lines.splice(startLine, endLine - startLine, ...replacement.split('\n'));
|
|
125
|
+
}
|
|
126
|
+
return lines.join('\n') as string_book;
|
|
127
|
+
}
|