@brunobracaioli/aibrain 0.4.0-beta.13 → 0.4.0-beta.15
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 +15 -1
- package/dist/claude-observation.mjs +1 -1
- package/dist/cli.mjs +17 -3
- package/dist/engineering-init.mjs +613 -0
- package/dist/engineering-pack.mjs +8 -1
- package/dist/extensions-catalog.mjs +49 -4
- package/dist/extensions-cli.mjs +8 -2
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,6 +30,19 @@ roles. Nothing is written to `~/.codex`, `~/.agents` or the project, and the
|
|
|
30
30
|
session's folder is deleted on exit. An older Codex starts without the pack and
|
|
31
31
|
says `codex_outdated`.
|
|
32
32
|
|
|
33
|
+
`aisac init [dir]` (licensed accounts, client `0.4.0-beta.14` or later) writes the
|
|
34
|
+
AISAC skeleton into a new or existing project: `CLAUDE.md`, `AGENTS.md`,
|
|
35
|
+
`NOTES.example.md`, the `docs/` tree with its templates, a Claude Code rules file,
|
|
36
|
+
a CI workflow with pinned actions, Dependabot, `.gitignore` and the workloop
|
|
37
|
+
template. The templates travel inside the licensed pack, fetched and verified like
|
|
38
|
+
a launch; the command writes only files that are missing, never follows a symbolic
|
|
39
|
+
link, never touches `.git/` and prints every file it wrote or skipped. Options:
|
|
40
|
+
`--stack node|python|none` (otherwise detected from `package.json`, `pyproject.toml`
|
|
41
|
+
or `requirements.txt`), `--lang pt-BR|en`, `--dry-run`, `--json`. Exit codes: `0`
|
|
42
|
+
when every file exists or was written, `1` for a license, pack or write failure
|
|
43
|
+
(nothing is written before the pack is verified), `2` for a usage error. Until the
|
|
44
|
+
pack carries the templates, the command says so and writes nothing.
|
|
45
|
+
|
|
33
46
|
Licensed `aisac claude` sessions also load guardrail hooks as the session plugin
|
|
34
47
|
`aisac-guardrails`.
|
|
35
48
|
- **Default on:** `secrets-guard` refuses secret files by name. Four more hooks
|
|
@@ -50,7 +63,8 @@ Licensed `aisac claude` sessions also load guardrail hooks as the session plugin
|
|
|
50
63
|
|
|
51
64
|
Licensed sessions of `aisac claude` and `aisac codex` can also load reviewed MCP
|
|
52
65
|
servers: `github` (read-only, token in `GITHUB_PERSONAL_ACCESS_TOKEN`), `supabase`
|
|
53
|
-
(read-only), `vercel`, `playwright` (pinned, isolated) and `
|
|
66
|
+
(read-only), `vercel`, `playwright` (pinned, isolated), `context7` and `creatives` (the
|
|
67
|
+
AISAC Creatives Studio, from client `0.4.0-beta.15`).
|
|
54
68
|
- **Default:** every server is off. `aisac mcp` lists them, `aisac mcp enable|disable
|
|
55
69
|
<id>` or `aisac mcp reset` changes the next session, and `aisac mcp info <id>` says
|
|
56
70
|
what a server reaches and how to log in.
|
|
@@ -16,7 +16,7 @@ export const MAX_LOG_FILES = 3;
|
|
|
16
16
|
const LOCK_WAIT_MS = 250;
|
|
17
17
|
const LOCK_STALE_MS = 30_000;
|
|
18
18
|
const ADMISSION_MS = 60 * 60 * 1000;
|
|
19
|
-
export const ADAPTER_VERSION = '0.4.0-beta.
|
|
19
|
+
export const ADAPTER_VERSION = '0.4.0-beta.15';
|
|
20
20
|
const MAX_DIAGNOSTIC_COUNT = 10_000_000;
|
|
21
21
|
const MAX_DIAGNOSTIC_BYTES = 128 * 1024 * 1024;
|
|
22
22
|
const CHECKPOINT_BACKENDS = new Set(['native_store', 'private_file']);
|
package/dist/cli.mjs
CHANGED
|
@@ -37,13 +37,14 @@ import {
|
|
|
37
37
|
resolvePackLocale,
|
|
38
38
|
writeStoredLocale,
|
|
39
39
|
} from './engineering-pack.mjs';
|
|
40
|
+
import { initCommand } from './engineering-init.mjs';
|
|
40
41
|
import { extensionsSummary, hooksCommand, mcpCommand } from './extensions-cli.mjs';
|
|
41
42
|
import { sessionServers } from './mcp-servers.mjs';
|
|
42
43
|
import { anyFormatter } from './guardrails/format.mjs';
|
|
43
44
|
import { readCodexGuardrails } from './guardrails-plugin.mjs';
|
|
44
45
|
import { codexHooksStatus, supportsCodexHooks } from './codex-hooks.mjs';
|
|
45
46
|
|
|
46
|
-
const VERSION = '0.4.0-beta.
|
|
47
|
+
const VERSION = '0.4.0-beta.15';
|
|
47
48
|
// The command is `aisac`; `aibrain` is an alias installed from the same entry.
|
|
48
49
|
const COMMAND = 'aisac';
|
|
49
50
|
const ENDPOINT = 'https://mcp.ai-brain.aisac.cloud/mcp';
|
|
@@ -52,7 +53,7 @@ const LEGACY_DISCLOSURE_VERSION = 'aibrain-paid-context-v1-2026-10-09';
|
|
|
52
53
|
|
|
53
54
|
function usage() {
|
|
54
55
|
console.log(
|
|
55
|
-
`${COMMAND} ${VERSION}\n\nUsage:\n ${COMMAND} setup [--client claude|codex|both] [--context-profile upstream|conservative]\n [--compact-at 1..95] [--keep-threshold 0..1] [--preserve-recent 1..100]\n [--min-reduction 0..1] [--truncate-head-chars 0..4096]\n [--max-state-tokens 1000..25000] [--max-request-tokens 1000..30000]\n [--tool-policy compatible|read_only] [--goal TEXT]\n ${COMMAND} doctor [--json]\n ${COMMAND} status [--json]\n ${COMMAND} disable\n ${COMMAND} setup --undo\n ${COMMAND} claude [-- native-args...]\n ${COMMAND} codex [-- native-args...]\n\nPolicy notes:\n Automatic monitoring asks for compaction at compact-at percent; state/request\n limits are estimated upper bounds and do not override server quotas.\n min-reduction, truncate-head-chars and tool-policy apply only to Claude;\n Codex keeps native compaction and same-session recovery.`,
|
|
56
|
+
`${COMMAND} ${VERSION}\n\nUsage:\n ${COMMAND} setup [--client claude|codex|both] [--context-profile upstream|conservative]\n [--compact-at 1..95] [--keep-threshold 0..1] [--preserve-recent 1..100]\n [--min-reduction 0..1] [--truncate-head-chars 0..4096]\n [--max-state-tokens 1000..25000] [--max-request-tokens 1000..30000]\n [--tool-policy compatible|read_only] [--goal TEXT]\n ${COMMAND} doctor [--json]\n ${COMMAND} status [--json]\n ${COMMAND} disable\n ${COMMAND} setup --undo\n ${COMMAND} claude [-- native-args...]\n ${COMMAND} codex [-- native-args...]\n ${COMMAND} init [dir] [--lang pt-BR|en] [--stack node|python|none] [--dry-run] [--json]\n\nPolicy notes:\n Automatic monitoring asks for compaction at compact-at percent; state/request\n limits are estimated upper bounds and do not override server quotas.\n min-reduction, truncate-head-chars and tool-policy apply only to Claude;\n Codex keeps native compaction and same-session recovery.`,
|
|
56
57
|
);
|
|
57
58
|
console.log(
|
|
58
59
|
'Clients: setup without --client configures the clients installed on PATH (Claude Code, Codex, or both).',
|
|
@@ -66,6 +67,9 @@ function usage() {
|
|
|
66
67
|
console.log(
|
|
67
68
|
'AISAC Engineering: `aisac claude` and `aisac codex` (Codex 0.164+) load the licensed method; setup --lang pt-BR|en picks its language (before the first setup it runs the full setup); setup and doctor report the license; status stays offline. `aibrain` is an alias of `aisac`.',
|
|
68
69
|
);
|
|
70
|
+
console.log(
|
|
71
|
+
'Project skeleton: `aisac init [dir]` (licensed accounts) writes the AISAC skeleton from the pack into a new or existing project, only the files that are missing; `--dry-run` lists them, `--stack` picks node|python|none when detection is not enough.',
|
|
72
|
+
);
|
|
69
73
|
console.log(
|
|
70
74
|
'Guardrails: `aisac hooks` lists them (a checklist on a terminal); `aisac hooks enable|disable <id>...` and `aisac hooks reset` change the next `aisac claude` or `aisac codex` session.',
|
|
71
75
|
);
|
|
@@ -845,6 +849,14 @@ async function main(argv) {
|
|
|
845
849
|
}
|
|
846
850
|
if (command === 'hooks') return hooksCommand(rest);
|
|
847
851
|
if (command === 'mcp') return mcpCommand(rest);
|
|
852
|
+
if (command === 'init')
|
|
853
|
+
return initCommand(rest, {
|
|
854
|
+
packageRoot: PACKAGE_ROOT,
|
|
855
|
+
readConfig,
|
|
856
|
+
stateRoot,
|
|
857
|
+
cwd: process.cwd(),
|
|
858
|
+
env: process.env,
|
|
859
|
+
});
|
|
848
860
|
if (command === 'disable') {
|
|
849
861
|
if (rest.length > 0) throw new Error('disable_does_not_accept_options');
|
|
850
862
|
return removeOwnedSetup();
|
|
@@ -897,5 +909,7 @@ main(cliArgs).catch((error) => {
|
|
|
897
909
|
const guide = /** @type {{ guide?: unknown }} */ (error ?? {}).guide;
|
|
898
910
|
if (typeof guide === 'string') console.error(`\n${guide}`);
|
|
899
911
|
}
|
|
900
|
-
|
|
912
|
+
// Usage errors of `aisac init` exit 2 (spec FR3); everything else exits 1.
|
|
913
|
+
const exitCode = /** @type {{ exitCode?: unknown }} */ (error ?? {}).exitCode;
|
|
914
|
+
process.exitCode = exitCode === 2 ? 2 : 1;
|
|
901
915
|
});
|
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `aisac init` (spec aisac-engineering-init, ADR-0053, wave E5c): the AISAC
|
|
5
|
+
* skeleton written into the user's project from the licensed pack's `init/`
|
|
6
|
+
* templates.
|
|
7
|
+
*
|
|
8
|
+
* A template is one pack file under `init/` whose front matter names the
|
|
9
|
+
* target path and the stack, and whose body holds exactly one fenced code
|
|
10
|
+
* block: the fenced lines are the file. Only that body is written, so the
|
|
11
|
+
* per-account watermark the server adds to every front matter never reaches
|
|
12
|
+
* a repository. Only missing files are written, with exclusive creates, never
|
|
13
|
+
* through a symbolic link and never under `.git/`. The command fetches and
|
|
14
|
+
* verifies the pack with the same code path as a launch; nothing is written
|
|
15
|
+
* before the pack is verified, and nothing is written outside the directory
|
|
16
|
+
* the user named.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { lstat, mkdir, open, realpath, stat } from 'node:fs/promises';
|
|
20
|
+
import { basename, join, resolve, sep } from 'node:path';
|
|
21
|
+
import {
|
|
22
|
+
INIT_PREFIX,
|
|
23
|
+
isPackLocale,
|
|
24
|
+
obtainPack,
|
|
25
|
+
packNotice,
|
|
26
|
+
readStoredLocale,
|
|
27
|
+
resolvePackLocale,
|
|
28
|
+
splitFrontMatter,
|
|
29
|
+
} from './engineering-pack.mjs';
|
|
30
|
+
import { isRecord } from './engineering.mjs';
|
|
31
|
+
|
|
32
|
+
export const INIT_STACKS = /** @type {const} */ (['node', 'python', 'none']);
|
|
33
|
+
const TEMPLATE_STACKS = new Set([...INIT_STACKS, 'all']);
|
|
34
|
+
const TEMPLATE_PATH = /^init\/([A-Za-z0-9][A-Za-z0-9._-]{0,127})\.md$/u;
|
|
35
|
+
const TEMPLATE_KEYS = new Set(['name', 'description', 'target', 'stack', 'aisac_watermark']);
|
|
36
|
+
const MAX_DESCRIPTION_LENGTH = 1024;
|
|
37
|
+
const MAX_TARGET_LENGTH = 200;
|
|
38
|
+
const TARGET_CHARACTERS = /^[A-Za-z0-9._/-]+$/u;
|
|
39
|
+
const FORBIDDEN_SEGMENTS = new Set(['.', '..', '.git']);
|
|
40
|
+
const OPENING_FENCE = /^(`{3,})([^`]*)$/u;
|
|
41
|
+
const CLOSING_FENCE = /^(`{3,})[ \t]*$/u;
|
|
42
|
+
const WATERMARK_KEY = 'aisac_watermark';
|
|
43
|
+
const PROJECT_NAME = /^[A-Za-z0-9._-]{1,64}$/u;
|
|
44
|
+
const PROJECT_NAME_TOKEN = '{{project_name}}';
|
|
45
|
+
const DEFAULT_PROJECT_NAME = 'project';
|
|
46
|
+
/** YAML and `.gitignore` templates are byte-identical across locales and carry no token (spec D7, D8). */
|
|
47
|
+
const CONFIGURATION_TARGET = /(?:\.ya?ml|\.gitignore)$/u;
|
|
48
|
+
const DIRECTORY_MODE = 0o755;
|
|
49
|
+
const FILE_MODE = 0o644;
|
|
50
|
+
const NODE_MANIFEST = 'package.json';
|
|
51
|
+
const PYTHON_MANIFESTS = ['pyproject.toml', 'requirements.txt'];
|
|
52
|
+
const CLAUDE_MEMORY_FILE = 'CLAUDE.md';
|
|
53
|
+
const USAGE_EXIT_CODE = 2;
|
|
54
|
+
const UPDATE_COMMAND = 'npm install -g @aisac/cli@beta';
|
|
55
|
+
|
|
56
|
+
/** @typedef {(typeof INIT_STACKS)[number]} InitStack */
|
|
57
|
+
/** @typedef {InitStack | 'all'} TemplateStack */
|
|
58
|
+
/** @typedef {import('./engineering-pack.mjs').PackLocale} PackLocale */
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {{
|
|
61
|
+
* path: string,
|
|
62
|
+
* name: string,
|
|
63
|
+
* description: string,
|
|
64
|
+
* target: string,
|
|
65
|
+
* stack: TemplateStack,
|
|
66
|
+
* body: string
|
|
67
|
+
* }} InitTemplate
|
|
68
|
+
*/
|
|
69
|
+
/** @typedef {'init_path' | 'init_front_matter' | 'init_target' | 'init_body'} InitTemplateReason */
|
|
70
|
+
/** @typedef {'wrote' | 'skipped' | 'failed'} InitOutcome */
|
|
71
|
+
/** @typedef {{ target: string, outcome: InitOutcome, reason: string | null }} InitEntry */
|
|
72
|
+
/**
|
|
73
|
+
* @typedef {{
|
|
74
|
+
* directory: string,
|
|
75
|
+
* entries: InitEntry[],
|
|
76
|
+
* wrote: string[],
|
|
77
|
+
* skipped: { target: string, reason: string }[],
|
|
78
|
+
* failed: { target: string, reason: string }[]
|
|
79
|
+
* }} InitResult
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/** A template the server should never have served; the whole run stops. */
|
|
83
|
+
export class InitTemplateError extends Error {
|
|
84
|
+
/** @param {InitTemplateReason} reason @param {string} path */
|
|
85
|
+
constructor(reason, path) {
|
|
86
|
+
super(`init_template_invalid:${reason}:${path}`);
|
|
87
|
+
this.reason = reason;
|
|
88
|
+
this.path = path;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @param {string} code */
|
|
93
|
+
function usageError(code) {
|
|
94
|
+
const error = new Error(code);
|
|
95
|
+
Object.defineProperty(error, 'exitCode', { value: USAGE_EXIT_CODE });
|
|
96
|
+
return error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** @param {unknown} value @returns {value is InitStack} */
|
|
100
|
+
export function isInitStack(value) {
|
|
101
|
+
return typeof value === 'string' && INIT_STACKS.includes(/** @type {InitStack} */ (value));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Relative, `[A-Za-z0-9._/-]`, at most 200 characters, no empty, `.`, `..` or
|
|
106
|
+
* `.git` segment (spec FR2). A leading dot inside a segment is fine (`.github`).
|
|
107
|
+
* @param {unknown} target
|
|
108
|
+
* @returns {target is string}
|
|
109
|
+
*/
|
|
110
|
+
export function isValidInitTarget(target) {
|
|
111
|
+
if (typeof target !== 'string' || target.length === 0 || target.length > MAX_TARGET_LENGTH)
|
|
112
|
+
return false;
|
|
113
|
+
if (!TARGET_CHARACTERS.test(target)) return false;
|
|
114
|
+
return target
|
|
115
|
+
.split('/')
|
|
116
|
+
.every((segment) => segment.length > 0 && !FORBIDDEN_SEGMENTS.has(segment.toLowerCase()));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The one fenced block of a template body, as file content: its lines, each
|
|
121
|
+
* ended by a newline; an empty block is an empty file. Anything outside the
|
|
122
|
+
* block is the author's note. A longer outer fence keeps inner fences as
|
|
123
|
+
* content, so a template can carry Markdown with code blocks.
|
|
124
|
+
* @param {string} markdown
|
|
125
|
+
* @param {string} path
|
|
126
|
+
*/
|
|
127
|
+
function fencedBody(markdown, path) {
|
|
128
|
+
/** @type {number | null} */
|
|
129
|
+
let fence = null;
|
|
130
|
+
/** @type {string[] | null} */
|
|
131
|
+
let content = null;
|
|
132
|
+
let closed = false;
|
|
133
|
+
for (const line of markdown.split('\n')) {
|
|
134
|
+
if (fence === null) {
|
|
135
|
+
const opening = OPENING_FENCE.exec(line);
|
|
136
|
+
if (opening === null) continue;
|
|
137
|
+
if (closed) throw new InitTemplateError('init_body', path);
|
|
138
|
+
fence = opening[1]?.length ?? 0;
|
|
139
|
+
content = [];
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const closing = CLOSING_FENCE.exec(line);
|
|
143
|
+
if (closing !== null && (closing[1]?.length ?? 0) >= fence) {
|
|
144
|
+
fence = null;
|
|
145
|
+
closed = true;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
content?.push(line);
|
|
149
|
+
}
|
|
150
|
+
if (fence !== null || !closed || content === null) throw new InitTemplateError('init_body', path);
|
|
151
|
+
if (content.some((line) => line.includes(WATERMARK_KEY)))
|
|
152
|
+
throw new InitTemplateError('init_body', path);
|
|
153
|
+
return content.length === 0 ? '' : `${content.join('\n')}\n`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Parse one `init/<name>.md` pack file (spec FR2). The server validates the
|
|
158
|
+
* same rules before serving; this is the second layer.
|
|
159
|
+
* @param {{ path: string, content: string }} file
|
|
160
|
+
* @returns {InitTemplate}
|
|
161
|
+
*/
|
|
162
|
+
export function parseInitTemplate(file) {
|
|
163
|
+
const stem = TEMPLATE_PATH.exec(file.path)?.[1];
|
|
164
|
+
if (stem === undefined) throw new InitTemplateError('init_path', file.path);
|
|
165
|
+
const parsed = splitFrontMatter(file.content);
|
|
166
|
+
if (parsed === null) throw new InitTemplateError('init_front_matter', file.path);
|
|
167
|
+
const { values, body } = parsed;
|
|
168
|
+
for (const key of values.keys()) {
|
|
169
|
+
if (!TEMPLATE_KEYS.has(key)) throw new InitTemplateError('init_front_matter', file.path);
|
|
170
|
+
}
|
|
171
|
+
const name = values.get('name');
|
|
172
|
+
const description = values.get('description');
|
|
173
|
+
const stack = values.get('stack') ?? 'all';
|
|
174
|
+
if (
|
|
175
|
+
name !== stem ||
|
|
176
|
+
description === undefined ||
|
|
177
|
+
description.length === 0 ||
|
|
178
|
+
description.length > MAX_DESCRIPTION_LENGTH ||
|
|
179
|
+
description === '>' ||
|
|
180
|
+
description === '|' ||
|
|
181
|
+
!TEMPLATE_STACKS.has(stack)
|
|
182
|
+
)
|
|
183
|
+
throw new InitTemplateError('init_front_matter', file.path);
|
|
184
|
+
const target = values.get('target');
|
|
185
|
+
if (!isValidInitTarget(target)) throw new InitTemplateError('init_target', file.path);
|
|
186
|
+
return {
|
|
187
|
+
path: file.path,
|
|
188
|
+
name,
|
|
189
|
+
description,
|
|
190
|
+
target,
|
|
191
|
+
stack: /** @type {TemplateStack} */ (stack),
|
|
192
|
+
body: fencedBody(body, file.path),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** @param {string} left @param {string} right */
|
|
197
|
+
function compareTargets(left, right) {
|
|
198
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Every `init/` template of a verified pack, in sorted target order. Two
|
|
203
|
+
* templates may share a target only with different stacks, neither `all`; no
|
|
204
|
+
* target may be a directory of another.
|
|
205
|
+
* @param {readonly { path: string, content: string }[]} files
|
|
206
|
+
* @returns {InitTemplate[]}
|
|
207
|
+
*/
|
|
208
|
+
export function loadInitTemplates(files) {
|
|
209
|
+
const templates = files
|
|
210
|
+
.filter((file) => file.path.startsWith(INIT_PREFIX))
|
|
211
|
+
.map((file) => parseInitTemplate(file))
|
|
212
|
+
.sort(
|
|
213
|
+
(left, right) =>
|
|
214
|
+
compareTargets(left.target, right.target) || compareTargets(left.stack, right.stack),
|
|
215
|
+
);
|
|
216
|
+
/** @type {Map<string, InitTemplate[]>} */
|
|
217
|
+
const byTarget = new Map();
|
|
218
|
+
const directories = new Set();
|
|
219
|
+
for (const template of templates) {
|
|
220
|
+
const segments = template.target.split('/');
|
|
221
|
+
for (let depth = 1; depth < segments.length; depth += 1)
|
|
222
|
+
directories.add(segments.slice(0, depth).join('/'));
|
|
223
|
+
const sharing = byTarget.get(template.target) ?? [];
|
|
224
|
+
for (const other of sharing) {
|
|
225
|
+
if (other.stack === template.stack || other.stack === 'all' || template.stack === 'all')
|
|
226
|
+
throw new InitTemplateError('init_target', template.path);
|
|
227
|
+
}
|
|
228
|
+
sharing.push(template);
|
|
229
|
+
byTarget.set(template.target, sharing);
|
|
230
|
+
}
|
|
231
|
+
for (const template of templates) {
|
|
232
|
+
if (directories.has(template.target)) throw new InitTemplateError('init_target', template.path);
|
|
233
|
+
}
|
|
234
|
+
return templates;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The templates of one stack: its own plus the shared ones.
|
|
239
|
+
* @param {readonly InitTemplate[]} templates
|
|
240
|
+
* @param {InitStack} stack
|
|
241
|
+
*/
|
|
242
|
+
export function selectTemplates(templates, stack) {
|
|
243
|
+
return templates.filter((template) => template.stack === 'all' || template.stack === stack);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** @param {string} path */
|
|
247
|
+
async function isFile(path) {
|
|
248
|
+
try {
|
|
249
|
+
return (await stat(path)).isFile();
|
|
250
|
+
} catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* `node` from `package.json`, `python` from `pyproject.toml` or
|
|
257
|
+
* `requirements.txt`, `none` without either, `ambiguous` with both (spec D6).
|
|
258
|
+
* A missing directory has no stack.
|
|
259
|
+
* @param {string} directory
|
|
260
|
+
* @returns {Promise<InitStack | 'ambiguous'>}
|
|
261
|
+
*/
|
|
262
|
+
export async function detectStack(directory) {
|
|
263
|
+
const [node, ...python] = await Promise.all([
|
|
264
|
+
isFile(join(directory, NODE_MANIFEST)),
|
|
265
|
+
...PYTHON_MANIFESTS.map((manifest) => isFile(join(directory, manifest))),
|
|
266
|
+
]);
|
|
267
|
+
const hasPython = python.some(Boolean);
|
|
268
|
+
if (node && hasPython) return 'ambiguous';
|
|
269
|
+
if (node) return 'node';
|
|
270
|
+
return hasPython ? 'python' : 'none';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The directory's base name when it fits the token charset, else `project`
|
|
275
|
+
* (spec D7).
|
|
276
|
+
* @param {string} directory
|
|
277
|
+
*/
|
|
278
|
+
export function projectNameOf(directory) {
|
|
279
|
+
const name = basename(directory);
|
|
280
|
+
return PROJECT_NAME.test(name) ? name : DEFAULT_PROJECT_NAME;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The file content for one template: the fenced body with `{{project_name}}`
|
|
285
|
+
* replaced in text templates; YAML and `.gitignore` are written as they are.
|
|
286
|
+
* @param {InitTemplate} template
|
|
287
|
+
* @param {string} projectName
|
|
288
|
+
*/
|
|
289
|
+
export function renderTemplate(template, projectName) {
|
|
290
|
+
return CONFIGURATION_TARGET.test(template.target)
|
|
291
|
+
? template.body
|
|
292
|
+
: template.body.replaceAll(PROJECT_NAME_TOKEN, projectName);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** @param {unknown} error */
|
|
296
|
+
function errorCode(error) {
|
|
297
|
+
return isRecord(error) && typeof error.code === 'string' ? error.code : 'write_failed';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Create the missing parents of a target without following links. A
|
|
302
|
+
* symbolic link on the way skips the file; a non-directory fails it.
|
|
303
|
+
* @param {string} base
|
|
304
|
+
* @param {readonly string[]} segments
|
|
305
|
+
* @param {boolean} dryRun
|
|
306
|
+
* @returns {Promise<{ outcome: 'ok' } | { outcome: 'skipped' | 'failed', reason: string }>}
|
|
307
|
+
*/
|
|
308
|
+
async function ensureParents(base, segments, dryRun) {
|
|
309
|
+
let current = base;
|
|
310
|
+
for (const segment of segments) {
|
|
311
|
+
current = join(current, segment);
|
|
312
|
+
let entry = await lstat(current).catch(() => null);
|
|
313
|
+
if (entry === null) {
|
|
314
|
+
if (dryRun) continue;
|
|
315
|
+
try {
|
|
316
|
+
await mkdir(current, { mode: DIRECTORY_MODE });
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (errorCode(error) !== 'EEXIST') return { outcome: 'failed', reason: errorCode(error) };
|
|
319
|
+
}
|
|
320
|
+
entry = await lstat(current).catch(() => null);
|
|
321
|
+
if (entry === null) return { outcome: 'failed', reason: 'parent_missing' };
|
|
322
|
+
}
|
|
323
|
+
if (entry.isSymbolicLink()) return { outcome: 'skipped', reason: 'symlink' };
|
|
324
|
+
if (!entry.isDirectory()) return { outcome: 'failed', reason: 'parent_not_directory' };
|
|
325
|
+
}
|
|
326
|
+
return { outcome: 'ok' };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Write the selected templates into the directory: missing files only,
|
|
331
|
+
* exclusive creates, `0644` files and `0755` directories, sorted target
|
|
332
|
+
* order, each file reported once. With `dryRun` nothing is created and the
|
|
333
|
+
* files that would be written are listed as written.
|
|
334
|
+
* @param {{ directory: string, templates: readonly InitTemplate[], projectName: string, dryRun: boolean }} input
|
|
335
|
+
* @returns {Promise<InitResult>}
|
|
336
|
+
*/
|
|
337
|
+
export async function applySkeleton(input) {
|
|
338
|
+
const base = input.dryRun
|
|
339
|
+
? await realpath(input.directory).catch(() => resolve(input.directory))
|
|
340
|
+
: await realpath(input.directory);
|
|
341
|
+
/** @type {InitEntry[]} */
|
|
342
|
+
const entries = [];
|
|
343
|
+
for (const template of input.templates) {
|
|
344
|
+
const segments = template.target.split('/');
|
|
345
|
+
const full = resolve(base, ...segments);
|
|
346
|
+
if (!full.startsWith(`${base}${sep}`)) {
|
|
347
|
+
entries.push({ target: template.target, outcome: 'failed', reason: 'target_escape' });
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const parents = await ensureParents(base, segments.slice(0, -1), input.dryRun);
|
|
351
|
+
if (parents.outcome !== 'ok') {
|
|
352
|
+
entries.push({ target: template.target, outcome: parents.outcome, reason: parents.reason });
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const existing = await lstat(full).catch(() => null);
|
|
356
|
+
if (existing !== null) {
|
|
357
|
+
entries.push({
|
|
358
|
+
target: template.target,
|
|
359
|
+
outcome: 'skipped',
|
|
360
|
+
reason: existing.isSymbolicLink() ? 'symlink' : 'exists',
|
|
361
|
+
});
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (input.dryRun) {
|
|
365
|
+
entries.push({ target: template.target, outcome: 'wrote', reason: null });
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
const handle = await open(full, 'wx', FILE_MODE);
|
|
370
|
+
try {
|
|
371
|
+
await handle.writeFile(renderTemplate(template, input.projectName), 'utf8');
|
|
372
|
+
} finally {
|
|
373
|
+
await handle.close();
|
|
374
|
+
}
|
|
375
|
+
entries.push({ target: template.target, outcome: 'wrote', reason: null });
|
|
376
|
+
} catch (error) {
|
|
377
|
+
const code = errorCode(error);
|
|
378
|
+
entries.push(
|
|
379
|
+
code === 'EEXIST'
|
|
380
|
+
? { target: template.target, outcome: 'skipped', reason: 'exists' }
|
|
381
|
+
: { target: template.target, outcome: 'failed', reason: code },
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
directory: base,
|
|
387
|
+
entries,
|
|
388
|
+
wrote: entries.filter((entry) => entry.outcome === 'wrote').map((entry) => entry.target),
|
|
389
|
+
skipped: entries
|
|
390
|
+
.filter((entry) => entry.outcome === 'skipped')
|
|
391
|
+
.map((entry) => ({ target: entry.target, reason: entry.reason ?? 'exists' })),
|
|
392
|
+
failed: entries
|
|
393
|
+
.filter((entry) => entry.outcome === 'failed')
|
|
394
|
+
.map((entry) => ({ target: entry.target, reason: entry.reason ?? 'write_failed' })),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* The next steps after a run, in the pack locale (spec FR3).
|
|
400
|
+
* @param {PackLocale} locale
|
|
401
|
+
* @param {InitResult} result
|
|
402
|
+
*/
|
|
403
|
+
export function nextSteps(locale, result) {
|
|
404
|
+
const pt = locale === 'pt-BR';
|
|
405
|
+
const claudeKept = result.skipped.some(
|
|
406
|
+
(entry) => entry.target === CLAUDE_MEMORY_FILE && entry.reason === 'exists',
|
|
407
|
+
);
|
|
408
|
+
return [
|
|
409
|
+
pt
|
|
410
|
+
? 'Copie NOTES.example.md para NOTES.md (o .gitignore já o exclui) e mantenha-o atualizado.'
|
|
411
|
+
: 'Copy NOTES.example.md to NOTES.md (already in .gitignore) and keep it current.',
|
|
412
|
+
pt
|
|
413
|
+
? 'Preencha os marcadores [CUSTOMIZE] de CLAUDE.md, AGENTS.md e .claude/rules/ na primeira sessão; o seu agente pode fazer isso.'
|
|
414
|
+
: 'Fill the [CUSTOMIZE] markers in CLAUDE.md, AGENTS.md and .claude/rules/ in the first session; your agent can do it.',
|
|
415
|
+
pt
|
|
416
|
+
? 'Rode aisac claude ou aisac codex nesta pasta: o método chega com a sessão.'
|
|
417
|
+
: 'Run aisac claude or aisac codex in this folder: the method arrives with the session.',
|
|
418
|
+
pt
|
|
419
|
+
? 'Leia docs/README.md para o mapa da documentação.'
|
|
420
|
+
: 'Read docs/README.md for the documentation map.',
|
|
421
|
+
...(claudeKept
|
|
422
|
+
? [
|
|
423
|
+
pt
|
|
424
|
+
? 'CLAUDE.md já existia e ficou como estava; peça à skill aisac-method para incorporar o método nele.'
|
|
425
|
+
: 'CLAUDE.md already existed and was left as is; ask the aisac-method skill to fold the method into it.',
|
|
426
|
+
]
|
|
427
|
+
: []),
|
|
428
|
+
];
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* The human report: one line per file in target order, a summary, the next steps.
|
|
433
|
+
* @param {{ result: InitResult, locale: PackLocale, stack: InitStack, packVersion: string, dryRun: boolean }} input
|
|
434
|
+
*/
|
|
435
|
+
export function initReport(input) {
|
|
436
|
+
const pt = input.locale === 'pt-BR';
|
|
437
|
+
const lines = input.result.entries.map((entry) => {
|
|
438
|
+
switch (entry.outcome) {
|
|
439
|
+
case 'wrote':
|
|
440
|
+
return input.dryRun
|
|
441
|
+
? `${pt ? 'gravaria' : 'would write'} ${entry.target}`
|
|
442
|
+
: `${pt ? 'gravado' : 'wrote'} ${entry.target}`;
|
|
443
|
+
case 'skipped':
|
|
444
|
+
return `${pt ? 'pulado' : 'skipped'} ${entry.target} (${entry.reason})`;
|
|
445
|
+
default:
|
|
446
|
+
return `${pt ? 'falhou' : 'failed'} ${entry.target} (${entry.reason})`;
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
const counts = pt
|
|
450
|
+
? `${input.result.wrote.length} ${input.dryRun ? 'a gravar' : 'gravados'}, ${input.result.skipped.length} pulados, ${input.result.failed.length} com falha`
|
|
451
|
+
: `${input.result.wrote.length} ${input.dryRun ? 'to write' : 'written'}, ${input.result.skipped.length} skipped, ${input.result.failed.length} failed`;
|
|
452
|
+
lines.push(
|
|
453
|
+
`aisac init: ${counts} (stack ${input.stack}, ${input.locale}, pack ${input.packVersion}) ${pt ? 'em' : 'in'} ${input.result.directory}`,
|
|
454
|
+
);
|
|
455
|
+
if (!input.dryRun && input.result.failed.length === 0) {
|
|
456
|
+
lines.push(pt ? 'Próximos passos:' : 'Next steps:');
|
|
457
|
+
for (const [index, step] of nextSteps(input.locale, input.result).entries())
|
|
458
|
+
lines.push(` ${index + 1}. ${step}`);
|
|
459
|
+
}
|
|
460
|
+
return lines;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* @typedef {{
|
|
465
|
+
* dir: string | undefined,
|
|
466
|
+
* lang: PackLocale | undefined,
|
|
467
|
+
* stack: InitStack | undefined,
|
|
468
|
+
* dryRun: boolean,
|
|
469
|
+
* json: boolean
|
|
470
|
+
* }} InitArguments
|
|
471
|
+
*/
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* `aisac init [dir] [--lang pt-BR|en] [--stack node|python|none] [--dry-run] [--json]`.
|
|
475
|
+
* Usage errors exit 2.
|
|
476
|
+
* @param {readonly string[]} args
|
|
477
|
+
* @returns {InitArguments}
|
|
478
|
+
*/
|
|
479
|
+
export function parseInitArguments(args) {
|
|
480
|
+
/** @type {InitArguments} */
|
|
481
|
+
const parsed = { dir: undefined, lang: undefined, stack: undefined, dryRun: false, json: false };
|
|
482
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
483
|
+
const argument = /** @type {string} */ (args[index]);
|
|
484
|
+
if (argument === '--dry-run' || argument === '--json') {
|
|
485
|
+
const key = argument === '--json' ? 'json' : 'dryRun';
|
|
486
|
+
if (parsed[key]) throw usageError(`duplicate_option:${argument}`);
|
|
487
|
+
parsed[key] = true;
|
|
488
|
+
} else if (argument === '--lang' || argument === '--stack') {
|
|
489
|
+
const value = args[index + 1];
|
|
490
|
+
if (value === undefined || value.startsWith('-'))
|
|
491
|
+
throw usageError(`option_value_required:${argument}`);
|
|
492
|
+
const key = argument === '--lang' ? 'lang' : 'stack';
|
|
493
|
+
if (parsed[key] !== undefined) throw usageError(`duplicate_option:${argument}`);
|
|
494
|
+
if (key === 'lang') {
|
|
495
|
+
if (!isPackLocale(value)) throw usageError('invalid_lang');
|
|
496
|
+
parsed.lang = value;
|
|
497
|
+
} else {
|
|
498
|
+
if (!isInitStack(value)) throw usageError('invalid_stack');
|
|
499
|
+
parsed.stack = value;
|
|
500
|
+
}
|
|
501
|
+
index += 1;
|
|
502
|
+
} else if (argument.startsWith('-') || parsed.dir !== undefined) {
|
|
503
|
+
throw usageError('unknown_option');
|
|
504
|
+
} else {
|
|
505
|
+
parsed.dir = argument;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return parsed;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* @typedef {{
|
|
513
|
+
* packageRoot: string,
|
|
514
|
+
* readConfig: () => Promise<unknown>,
|
|
515
|
+
* stateRoot: () => string,
|
|
516
|
+
* cwd?: string,
|
|
517
|
+
* env?: NodeJS.ProcessEnv,
|
|
518
|
+
* fetchImpl?: typeof fetch,
|
|
519
|
+
* log?: (line: string) => void,
|
|
520
|
+
* warn?: (line: string) => void
|
|
521
|
+
* }} InitDependencies
|
|
522
|
+
*/
|
|
523
|
+
|
|
524
|
+
/** @param {unknown} config @returns {{ endpoint: string, token: string }} */
|
|
525
|
+
function accountOf(config) {
|
|
526
|
+
if (
|
|
527
|
+
!isRecord(config) ||
|
|
528
|
+
typeof config.endpoint !== 'string' ||
|
|
529
|
+
typeof config.token !== 'string' ||
|
|
530
|
+
config.token.length === 0
|
|
531
|
+
)
|
|
532
|
+
throw new Error('setup_required');
|
|
533
|
+
return { endpoint: config.endpoint, token: config.token };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* The command. Throws with the error code as message; usage errors carry
|
|
538
|
+
* `exitCode` 2, everything else exits 1 through the CLI's handler.
|
|
539
|
+
* @param {readonly string[]} args
|
|
540
|
+
* @param {InitDependencies} deps
|
|
541
|
+
*/
|
|
542
|
+
export async function initCommand(args, deps) {
|
|
543
|
+
const log = deps.log ?? ((line) => console.log(line));
|
|
544
|
+
const warn = deps.warn ?? ((line) => console.error(line));
|
|
545
|
+
const options = parseInitArguments(args);
|
|
546
|
+
const directory = resolve(deps.cwd ?? process.cwd(), options.dir ?? '.');
|
|
547
|
+
const existing = await stat(directory).catch(() => null);
|
|
548
|
+
if (existing !== null && !existing.isDirectory()) throw usageError('dir_not_directory');
|
|
549
|
+
const detected = options.stack ?? (existing === null ? 'none' : await detectStack(directory));
|
|
550
|
+
if (detected === 'ambiguous') throw usageError('stack_ambiguous');
|
|
551
|
+
const stack = detected;
|
|
552
|
+
const account = accountOf(await deps.readConfig());
|
|
553
|
+
const locale =
|
|
554
|
+
options.lang ?? resolvePackLocale(await readStoredLocale(deps.stateRoot()), deps.env);
|
|
555
|
+
const outcome = await obtainPack({
|
|
556
|
+
endpoint: account.endpoint,
|
|
557
|
+
token: account.token,
|
|
558
|
+
locale,
|
|
559
|
+
packageRoot: deps.packageRoot,
|
|
560
|
+
...(deps.fetchImpl === undefined ? {} : { fetchImpl: deps.fetchImpl }),
|
|
561
|
+
});
|
|
562
|
+
if (outcome.outcome !== 'loaded') {
|
|
563
|
+
const notice = packNotice(outcome, locale);
|
|
564
|
+
if (notice !== null) warn(notice);
|
|
565
|
+
switch (outcome.outcome) {
|
|
566
|
+
case 'inactive':
|
|
567
|
+
throw new Error('license_inactive');
|
|
568
|
+
case 'not_offered':
|
|
569
|
+
throw new Error('engineering_not_offered');
|
|
570
|
+
case 'unavailable':
|
|
571
|
+
throw new Error(`pack_unavailable:${outcome.reason}`);
|
|
572
|
+
default:
|
|
573
|
+
throw new Error(`pack_rejected:${outcome.reason}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const templates = selectTemplates(loadInitTemplates(outcome.pack.files), stack);
|
|
577
|
+
if (templates.length === 0) {
|
|
578
|
+
warn(
|
|
579
|
+
locale === 'pt-BR'
|
|
580
|
+
? `AISAC Engineering: o pacote ainda não traz os modelos do init; atualize o aisac (${UPDATE_COMMAND}) ou tente mais tarde.`
|
|
581
|
+
: `AISAC Engineering: the pack carries no init templates yet; update aisac (${UPDATE_COMMAND}) or try again later.`,
|
|
582
|
+
);
|
|
583
|
+
throw new Error('init_templates_missing');
|
|
584
|
+
}
|
|
585
|
+
if (existing === null && !options.dryRun)
|
|
586
|
+
await mkdir(directory, { recursive: true, mode: DIRECTORY_MODE });
|
|
587
|
+
const result = await applySkeleton({
|
|
588
|
+
directory,
|
|
589
|
+
templates,
|
|
590
|
+
projectName: projectNameOf(await realpath(directory).catch(() => directory)),
|
|
591
|
+
dryRun: options.dryRun,
|
|
592
|
+
});
|
|
593
|
+
const packVersion = outcome.pack.packVersion;
|
|
594
|
+
if (options.json) {
|
|
595
|
+
log(
|
|
596
|
+
JSON.stringify({
|
|
597
|
+
dir: result.directory,
|
|
598
|
+
locale,
|
|
599
|
+
stack,
|
|
600
|
+
packVersion,
|
|
601
|
+
dryRun: options.dryRun,
|
|
602
|
+
wrote: result.wrote,
|
|
603
|
+
skipped: result.skipped,
|
|
604
|
+
failed: result.failed,
|
|
605
|
+
nextSteps: nextSteps(locale, result),
|
|
606
|
+
}),
|
|
607
|
+
);
|
|
608
|
+
} else {
|
|
609
|
+
for (const line of initReport({ result, locale, stack, packVersion, dryRun: options.dryRun }))
|
|
610
|
+
log(line);
|
|
611
|
+
}
|
|
612
|
+
if (result.failed.length > 0) throw new Error('init_write_failed');
|
|
613
|
+
}
|
|
@@ -94,6 +94,11 @@ const FORBIDDEN_FRONT_MATTER_KEYS = new Set([
|
|
|
94
94
|
const PLUGIN_NAME = 'aisac-engineering';
|
|
95
95
|
const PLUGIN_LICENSE = 'LicenseRef-AISAC-Engineering';
|
|
96
96
|
const PACK_DIRECTORY = 'engineering';
|
|
97
|
+
/**
|
|
98
|
+
* `aisac init` templates (spec aisac-engineering-init D2, E5c): pack files that
|
|
99
|
+
* never reach a session. Only `engineering-init.mjs` reads them.
|
|
100
|
+
*/
|
|
101
|
+
export const INIT_PREFIX = 'init/';
|
|
97
102
|
const OUTPUT_STYLES_PREFIX = 'output-styles/';
|
|
98
103
|
const SKILLS_DIRECTORY = 'skills';
|
|
99
104
|
const AGENT_FILE = /^agents\/([a-z][a-z0-9-]{0,63})\.md$/u;
|
|
@@ -534,7 +539,8 @@ const PLUGIN_DESCRIPTION = {
|
|
|
534
539
|
|
|
535
540
|
/**
|
|
536
541
|
* Write the pack files into `engineering/` of the launch's private directory:
|
|
537
|
-
* `0700` directories, `0600` files, exclusive creation, contained paths.
|
|
542
|
+
* `0700` directories, `0600` files, exclusive creation, contained paths. The
|
|
543
|
+
* `init/` templates stay out: they are for `aisac init`, not for a session.
|
|
538
544
|
* @param {VerifiedPack} pack
|
|
539
545
|
* @param {string} launchDirectory
|
|
540
546
|
* @returns {Promise<string>} the pack root
|
|
@@ -544,6 +550,7 @@ async function writePackFiles(pack, launchDirectory) {
|
|
|
544
550
|
await mkdir(base, { mode: 0o700 });
|
|
545
551
|
for (const file of pack.files) {
|
|
546
552
|
if (!isValidPackPath(file.path)) throw new Error('pack_path_invalid');
|
|
553
|
+
if (file.path.startsWith(INIT_PREFIX)) continue;
|
|
547
554
|
const target = resolve(base, ...file.path.split('/'));
|
|
548
555
|
if (!target.startsWith(`${base}${sep}`)) throw new Error('pack_path_escape');
|
|
549
556
|
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
@@ -185,10 +185,14 @@ export function hookIds() {
|
|
|
185
185
|
* @property {Readonly<Record<CatalogLocale, string>>} [loginNote] Why and how this login differs.
|
|
186
186
|
* @property {Readonly<Record<CatalogLocale, string>>} description
|
|
187
187
|
* @property {Readonly<Record<CatalogLocale, string>>} reaches What the server can reach (FR7).
|
|
188
|
-
* @property {readonly string[]} [blockedTools] Tools
|
|
189
|
-
*
|
|
188
|
+
* @property {readonly string[]} [blockedTools] Tools removed in both clients whatever the
|
|
189
|
+
* user's permission mode is (FR7.2): tools that spend money, or that would take a
|
|
190
|
+
* credential as an argument. Exact names.
|
|
190
191
|
* @property {readonly string[]} [blockedToolPrefixes] Name prefixes removed in Claude Code
|
|
191
192
|
* only, through a glob deny rule; Codex documents exact names only (FR7.2).
|
|
193
|
+
* @property {Readonly<Record<CatalogLocale, string>>} [blockedNote] Why the tools are
|
|
194
|
+
* blocked, when the reason is not spending; `aisac mcp info` prints it in place of the
|
|
195
|
+
* spending sentence (FR7.2 amendment of 2026-11-09).
|
|
192
196
|
*/
|
|
193
197
|
|
|
194
198
|
/** Session name of a catalog server, so it never collides with the user's own. */
|
|
@@ -196,8 +200,10 @@ export const MCP_SERVER_PREFIX = 'aisac-';
|
|
|
196
200
|
const PLAYWRIGHT_PACKAGE = '@playwright/mcp@0.0.83';
|
|
197
201
|
|
|
198
202
|
/**
|
|
199
|
-
* MCP catalog v1 (spec FR7; the owner chose the
|
|
200
|
-
*
|
|
203
|
+
* MCP catalog v1 (spec FR7; the owner chose the five vendor servers on 2026-11-03)
|
|
204
|
+
* plus `creatives`, the AISAC Creatives Studio (owner, 2026-11-09; spec
|
|
205
|
+
* aisac-engineering-reels FR8, wave E7c). The review of each entry (source,
|
|
206
|
+
* transport, login, tools, reach) is recorded in the extensions spec (FR7.1).
|
|
201
207
|
* @type {readonly McpEntry[]}
|
|
202
208
|
*/
|
|
203
209
|
export const MCP_CATALOG = Object.freeze([
|
|
@@ -315,6 +321,45 @@ export const MCP_CATALOG = Object.freeze([
|
|
|
315
321
|
'Só documentação pública de bibliotecas. O que o agente pergunta (nomes de bibliotecas e dúvidas) vai para a Upstash, então não coloque segredos nem código privado nessas perguntas. Funciona sem login, com limites de uso menores.',
|
|
316
322
|
},
|
|
317
323
|
},
|
|
324
|
+
{
|
|
325
|
+
id: 'creatives',
|
|
326
|
+
defaultOn: false,
|
|
327
|
+
clients: ['claude', 'codex'],
|
|
328
|
+
publisher: 'AISAC',
|
|
329
|
+
source: 'https://studio.aisac.agency/',
|
|
330
|
+
// The URL is the OAuth resource the Studio itself publishes in its
|
|
331
|
+
// protected-resource metadata (read 2026-11-09, reels spec M3). The Studio
|
|
332
|
+
// web app lives at studio.aisac.agency, but that domain is the browser origin
|
|
333
|
+
// only: the MCP issuer, audience and resource stay on this host, so a token
|
|
334
|
+
// obtained for this URL is the one the server accepts.
|
|
335
|
+
transport: { type: 'http', url: 'https://mcp-creatives.vercel.app/mcp' },
|
|
336
|
+
login: 'host-oauth',
|
|
337
|
+
// The key-management tools take or delete a provider key by name. A key typed
|
|
338
|
+
// into a tool call would stay in the session transcript, so keys are managed
|
|
339
|
+
// only on the Studio keys page (reels spec D2, FR8). The generate tools spend
|
|
340
|
+
// on the keys the user registered there: not blocked (FR7.2 amendment).
|
|
341
|
+
blockedTools: ['creatives_register_key', 'creatives_revoke_key'],
|
|
342
|
+
blockedNote: {
|
|
343
|
+
en: 'Provider keys are managed only on the Studio keys page (https://studio.aisac.agency/?studio=keys): a key typed into a tool call would stay in the session transcript.',
|
|
344
|
+
'pt-BR':
|
|
345
|
+
'As chaves dos provedores são gerenciadas só na página de chaves do Studio (https://studio.aisac.agency/?studio=keys): uma chave digitada numa chamada de ferramenta ficaria no transcript da sessão.',
|
|
346
|
+
},
|
|
347
|
+
loginNote: {
|
|
348
|
+
en: 'The Studio login needs an account with access (it comes with the AISAC Engineering purchase). Register your provider keys on the Studio keys page before the first generation; the server never takes a key as an argument.',
|
|
349
|
+
'pt-BR':
|
|
350
|
+
'O login do Studio exige uma conta com acesso (vem com a compra do AISAC Engineering). Registre as chaves dos seus provedores na página de chaves do Studio antes da primeira geração; o servidor nunca recebe uma chave como argumento.',
|
|
351
|
+
},
|
|
352
|
+
description: {
|
|
353
|
+
en: 'The AISAC Creatives Studio: images, narration and video generated with your own provider keys, plus voices, influencer profiles and reel productions.',
|
|
354
|
+
'pt-BR':
|
|
355
|
+
'O AISAC Creatives Studio: imagens, narração e vídeo gerados com as suas próprias chaves de provedor, mais vozes, perfis de influencer e produções de reels.',
|
|
356
|
+
},
|
|
357
|
+
reaches: {
|
|
358
|
+
en: 'Your Studio account only: the generations, assets, voice presets, influencer profiles, Motion projects and productions you own, and the fingerprints of the provider keys you registered (never the keys). Image, audio and video generation and the Motion stages spend on your own provider keys, within the limits the Studio enforces; a Studio production spends only after an approval in the Studio web app or in the Mothership. Registering and revoking keys is blocked by AISAC.',
|
|
359
|
+
'pt-BR':
|
|
360
|
+
'Só a sua conta do Studio: as gerações, assets, presets de voz, perfis de influencer, projetos Motion e produções que são seus, e as impressões digitais das chaves de provedor que você registrou (nunca as chaves). Geração de imagem, áudio e vídeo e as etapas do Motion gastam das suas próprias chaves de provedor, dentro dos limites que o Studio impõe; uma produção do Studio só gasta depois de uma aprovação no app web do Studio ou no Mothership. Registrar e revogar chaves é bloqueado pela AISAC.',
|
|
361
|
+
},
|
|
362
|
+
},
|
|
318
363
|
]);
|
|
319
364
|
|
|
320
365
|
/** @param {string} id @returns {McpEntry | undefined} */
|
package/dist/extensions-cli.mjs
CHANGED
|
@@ -273,11 +273,13 @@ export function serverInfo(entry, locale, item) {
|
|
|
273
273
|
note: 'Hosted by the vendor: its tools can change without a client release, so keep the confirmation of each call on.',
|
|
274
274
|
},
|
|
275
275
|
reaches: entry.reaches[locale],
|
|
276
|
-
// Tools
|
|
276
|
+
// Tools removed whatever the permission mode is (FR7.2): they spend money, or
|
|
277
|
+
// the entry's blockedNote says why.
|
|
277
278
|
blockedTools: [...(entry.blockedTools ?? [])],
|
|
278
279
|
...(entry.blockedToolPrefixes === undefined
|
|
279
280
|
? {}
|
|
280
281
|
: { blockedToolPrefixes: [...entry.blockedToolPrefixes] }),
|
|
282
|
+
...(entry.blockedNote === undefined ? {} : { blockedNote: entry.blockedNote[locale] }),
|
|
281
283
|
login,
|
|
282
284
|
...(variable === undefined ? {} : { tokenVariable: variable }),
|
|
283
285
|
...(entry.loginNote === undefined ? {} : { loginNote: entry.loginNote[locale] }),
|
|
@@ -304,8 +306,12 @@ function printInfo(info) {
|
|
|
304
306
|
prefixes.length === 0
|
|
305
307
|
? ''
|
|
306
308
|
: ` In Claude Code, also any tool whose name starts with ${prefixes.join(', ')}.`;
|
|
309
|
+
const reason =
|
|
310
|
+
info.blockedNote === undefined
|
|
311
|
+
? `Blocked by AISAC, because they spend money: ${info.blockedTools.join(', ')}.${claudeOnly}`
|
|
312
|
+
: `Blocked by AISAC: ${info.blockedTools.join(', ')}.${claudeOnly} ${info.blockedNote}`;
|
|
307
313
|
console.log(
|
|
308
|
-
|
|
314
|
+
`${reason} They stay blocked even if you allow every tool or skip permission prompts.`,
|
|
309
315
|
);
|
|
310
316
|
}
|
|
311
317
|
if (info.loginNote !== undefined) console.log(`Login: ${info.loginNote}`);
|
package/dist/package.json
CHANGED
package/package.json
CHANGED