akari-video 1.5.0-beta.5 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.mjs +2 -0
- package/src/kits.mjs +35 -0
- package/src/messages.mjs +1 -1
- package/src/playbook-command.mjs +16 -6
- package/src/recipe-command.mjs +128 -0
- package/src/status-command.mjs +11 -2
- package/src/status-core/events.mjs +34 -0
- package/src/status-core/review.mjs +3 -1
- package/src/status-core/status.mjs +3 -1
- package/src/store-command.mjs +4 -0
- package/vendor/.akari-capability-sources.json +3 -0
- package/vendor/docs/contract-2026-07-20-review-json-v1-annotation-model.md +4 -2
- package/vendor/docs/contract-2026-08-23-stroke-persistence.md +2 -0
- package/vendor/docs/contract-2026-10-15-playbook-v0.md +1 -1
- package/vendor/docs/contract-2026-10-16-recipes-and-connectors-v0.md +14 -7
- package/vendor/docs/contract-2026-10-16-showcase-v0.md +150 -0
- package/vendor/docs/contract-2026-10-16-template-v0.md +129 -0
- package/vendor/docs/contract-2026-10-16-vocabulary-v0.md +175 -0
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/edit-lint/src/lint/review.mjs +1 -1
- package/vendor/packages/i18n/locales/en/annotations.json +3 -3
- package/vendor/packages/i18n/locales/en/annotationscommon.json +1 -0
- package/vendor/packages/i18n/locales/en/cli.json +17 -1
- package/vendor/packages/i18n/locales/en/preview.json +15 -6
- package/vendor/packages/i18n/locales/en/project.json +6 -4
- package/vendor/packages/i18n/locales/en/surfaces.json +28 -0
- package/vendor/packages/i18n/locales/ja/annotations.json +3 -3
- package/vendor/packages/i18n/locales/ja/annotationscommon.json +1 -0
- package/vendor/packages/i18n/locales/ja/cli.json +17 -1
- package/vendor/packages/i18n/locales/ja/preview.json +13 -4
- package/vendor/packages/i18n/locales/ja/project.json +8 -6
- package/vendor/packages/i18n/locales/ja/surfaces.json +28 -0
- package/vendor/packages/project-scaffold/src/index.mjs +313 -4
- package/vendor/packages/schemas/akari-recipe.schema.json +19 -0
- package/vendor/packages/schemas/bin/validate-kit-manifest.mjs +38 -1
- package/vendor/packages/schemas/bin/validate-review.mjs +3 -3
- package/vendor/packages/schemas/capabilities.json +9 -0
- package/vendor/packages/schemas/connections.schema.json +15 -0
- package/vendor/packages/schemas/fixtures/review/invalid-strokes-content-rect-empty-sessionref/review.json +6 -0
- package/vendor/packages/schemas/fixtures/review/valid-strokes-direct/review.json +6 -0
- package/vendor/packages/schemas/kit-manifest.schema.json +11 -0
- package/vendor/packages/schemas/playbook.schema.json +27 -2
- package/vendor/packages/schemas/review.schema.json +2 -2
- package/vendor/packages/schemas/test/akari-recipe.test.mjs +52 -0
- package/vendor/packages/schemas/test/fixtures/akari-recipes/music.json +11 -0
- package/vendor/packages/schemas/test/fixtures/akari-recipes/three-d.json +11 -0
- package/vendor/packages/schemas/test/kit-manifest.test.mjs +28 -1
- package/vendor/packages/schemas/test/kit-recipes.test.mjs +35 -0
- package/vendor/packages/schemas/test/validate-review.test.mjs +66 -0
- package/vendor/packages/schemas/test/validate-template.test.mjs +55 -0
- package/vendor/packages/schemas/validate-akari-recipe.mjs +60 -0
- package/vendor/packages/schemas/validate-playbook.mjs +172 -12
- package/vendor/skills/akari/SKILL.md +7 -0
- package/vendor/templates/project-default/.akari/workflow.json +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "1.5.0
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/cli.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import { runWordBookCommand } from './word-book-command.mjs';
|
|
|
32
32
|
import { runConnectCommand } from './connect-command.mjs';
|
|
33
33
|
import { runProjectCommand } from './project-command.mjs';
|
|
34
34
|
import { runPlaybookCommand } from './playbook-command.mjs';
|
|
35
|
+
import { runRecipeCommand } from './recipe-command.mjs';
|
|
35
36
|
import { resolveRuntimePaths } from './runtime-diagnostics.mjs';
|
|
36
37
|
import { cliText } from './i18n.mjs';
|
|
37
38
|
|
|
@@ -47,6 +48,7 @@ export const runSubcommands = {
|
|
|
47
48
|
connect: runConnectCommand,
|
|
48
49
|
project: runProjectCommand,
|
|
49
50
|
playbook: runPlaybookCommand,
|
|
51
|
+
recipe: runRecipeCommand,
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
/**
|
package/src/kits.mjs
CHANGED
|
@@ -293,6 +293,37 @@ export function linkKitPlaybooks(kitDir, manifest, home, options = {}) {
|
|
|
293
293
|
return { linked, warnings };
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
export function linkKitRecipes(kitDir, manifest, home, options = {}) {
|
|
297
|
+
const linked = [];
|
|
298
|
+
const warnings = [];
|
|
299
|
+
for (const entry of manifest.recipes ?? []) {
|
|
300
|
+
if (typeof entry?.dir !== 'string' || !/^recipes\/[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(entry.dir)) {
|
|
301
|
+
throw new Error(cliText('cli.kits.error.pathOutside', { path: String(entry?.dir) }));
|
|
302
|
+
}
|
|
303
|
+
const id = path.posix.basename(entry.dir);
|
|
304
|
+
const source = safeKitPath(kitDir, entry.dir);
|
|
305
|
+
const realKit = realpathSync(kitDir);
|
|
306
|
+
const realSource = realpathSync(source);
|
|
307
|
+
if (!realSource.startsWith(`${realKit}${path.sep}`) || !statSync(realSource).isDirectory()) {
|
|
308
|
+
throw new Error(cliText('cli.kits.error.pathOutside', { path: entry.dir }));
|
|
309
|
+
}
|
|
310
|
+
const destination = path.join(home, 'tool-recipes', id);
|
|
311
|
+
try {
|
|
312
|
+
const stat = lstatSync(destination);
|
|
313
|
+
let owned = false;
|
|
314
|
+
if (stat.isSymbolicLink()) {
|
|
315
|
+
try { owned = realpathSync(destination) === realSource; } catch { /* Broken links are not owned. */ }
|
|
316
|
+
}
|
|
317
|
+
if (!owned) { warnings.push(cliText('cli.kits.warning.directoryKept', { path: destination })); continue; }
|
|
318
|
+
} catch (error) { if (error?.code !== 'ENOENT') throw error; }
|
|
319
|
+
const result = replaceSymlink(source, destination, options);
|
|
320
|
+
if (result.status === 'linked') linked.push(id);
|
|
321
|
+
else warnings.push(cliText(result.status === 'permission-denied'
|
|
322
|
+
? 'cli.kits.warning.symlinkFailed' : 'cli.kits.warning.directoryKept', { path: destination }));
|
|
323
|
+
}
|
|
324
|
+
return { linked, warnings };
|
|
325
|
+
}
|
|
326
|
+
|
|
296
327
|
export function readKitsLedger(home) {
|
|
297
328
|
const ledgerPath = path.join(home, 'kits', 'installed.json');
|
|
298
329
|
if (!existsSync(ledgerPath)) return { schema: KITS_SCHEMA, kits: [] };
|
|
@@ -311,6 +342,9 @@ export function writeKitsLedger(home, entry) {
|
|
|
311
342
|
removeOwnedSymlink(path.join(home, 'playbooks', id), previous.kitDir);
|
|
312
343
|
}
|
|
313
344
|
}
|
|
345
|
+
for (const id of previous?.recipes ?? []) {
|
|
346
|
+
if (!(entry.recipes ?? []).includes(id)) removeOwnedSymlink(path.join(home, 'tool-recipes', id), previous.kitDir);
|
|
347
|
+
}
|
|
314
348
|
ledger.kits = [...ledger.kits.filter((kit) => kit.id !== entry.id), entry];
|
|
315
349
|
const ledgerPath = path.join(home, 'kits', 'installed.json');
|
|
316
350
|
mkdirSync(path.dirname(ledgerPath), { recursive: true });
|
|
@@ -326,6 +360,7 @@ export function removeKit(home, productId, env = process.env) {
|
|
|
326
360
|
if (!entry) return false;
|
|
327
361
|
for (const name of entry.skills ?? []) removeOwnedSymlink(path.join(home, 'kits', 'plugin', 'skills', name), entry.kitDir);
|
|
328
362
|
for (const id of entry.playbooks ?? []) removeOwnedSymlink(path.join(home, 'playbooks', id), entry.kitDir);
|
|
363
|
+
for (const id of entry.recipes ?? []) removeOwnedSymlink(path.join(home, 'tool-recipes', id), entry.kitDir);
|
|
329
364
|
for (const asset of entry.assets ?? []) {
|
|
330
365
|
for (const root of resolveAssetLibraryRoots({ ...env, AKARI_HOME: home }).read) removeOwnedSymlink(path.join(root, asset.category, asset.id), entry.kitDir);
|
|
331
366
|
}
|
package/src/messages.mjs
CHANGED
|
@@ -335,7 +335,7 @@ export function describeCliHelp() {
|
|
|
335
335
|
cliText('cli.help.header.usage'),
|
|
336
336
|
'',
|
|
337
337
|
cliText('cli.help.commands.heading'),
|
|
338
|
-
...['launch', 'store', 'sounds', 'doctor', 'speakers', 'toolsVoice', 'update', 'status', 'migrate', 'migrateLook', 'generate', 'storyboard', 'project', 'playbook', 'clean'].map((name) => cliText(`cli.help.commands.${name}`)),
|
|
338
|
+
...['launch', 'store', 'sounds', 'doctor', 'speakers', 'toolsVoice', 'update', 'status', 'migrate', 'migrateLook', 'generate', 'storyboard', 'project', 'playbook', 'recipe', 'clean'].map((name) => cliText(`cli.help.commands.${name}`)),
|
|
339
339
|
'',
|
|
340
340
|
cliText('cli.help.developer.heading'),
|
|
341
341
|
...['opencode', 'claude', 'yes', 'version', 'new', 'init', 'other', 'help'].map((name) => cliText(`cli.help.developer.${name}`)),
|
package/src/playbook-command.mjs
CHANGED
|
@@ -5,27 +5,30 @@ import { pathToFileURL } from 'node:url';
|
|
|
5
5
|
import { cliText } from './i18n.mjs';
|
|
6
6
|
import { resolveLauncherAssets } from './repo-assets.mjs';
|
|
7
7
|
|
|
8
|
-
const usage = () => cliText('cli.playbook.help.usage')
|
|
8
|
+
const usage = () => `${cliText('cli.playbook.help.usage')}\n${cliText('cli.playbook.template.usage')}`;
|
|
9
9
|
const safeChannel = value => typeof value === 'string' && value.trim() && value.length <= 64
|
|
10
10
|
&& value !== '.' && !value.includes('..') && !/[/\\<>:"|?*\x00-\x1f]/.test(value);
|
|
11
11
|
|
|
12
12
|
function parse(args) {
|
|
13
13
|
const action = args[0];
|
|
14
14
|
if (action === '--help' || action === '-h' || args.includes('--help') || args.includes('-h')) return { action: 'help' };
|
|
15
|
-
if (!['list', 'apply'].includes(action)) throw new Error(usage());
|
|
16
|
-
const result = { action, id: action === 'apply' ? args[1] : null };
|
|
15
|
+
if (!['list', 'apply', 'template'].includes(action)) throw new Error(usage());
|
|
16
|
+
const result = { action, id: action === 'apply' ? args[1] : null, project: action === 'template' ? args[1] : null };
|
|
17
17
|
if (action === 'apply' && (!result.id || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(result.id))) throw new Error(usage());
|
|
18
|
-
|
|
18
|
+
if (action === 'template' && !result.project) throw new Error(usage());
|
|
19
|
+
for (let i = action === 'apply' || action === 'template' ? 2 : 1; i < args.length; i++) {
|
|
19
20
|
const arg = args[i];
|
|
20
21
|
if ((action === 'list' && arg === '--json') || (action === 'apply' && arg === '--dry-run')) {
|
|
21
22
|
result[arg.slice(2).replace('-', '')] = true;
|
|
22
23
|
} else if ((action === 'list' && ['--category', '--tag', '--channel'].includes(arg))
|
|
23
|
-
|| (action === 'apply' && arg === '--into')
|
|
24
|
+
|| (action === 'apply' && arg === '--into')
|
|
25
|
+
|| (action === 'template' && ['--item', '--id', '--name', '--out', '--license-own'].includes(arg))) {
|
|
24
26
|
if (!args[i + 1] || args[i + 1].startsWith('--')) throw new Error(usage());
|
|
25
27
|
result[arg.slice(2)] = args[++i];
|
|
26
28
|
} else throw new Error(usage());
|
|
27
29
|
}
|
|
28
30
|
if (action === 'apply' && !result.into) throw new Error(usage());
|
|
31
|
+
if (action === 'template' && (!result.item || !result.id || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(result.id))) throw new Error(usage());
|
|
29
32
|
if (result.channel && !safeChannel(result.channel)) throw new Error(usage());
|
|
30
33
|
return result;
|
|
31
34
|
}
|
|
@@ -41,9 +44,16 @@ export async function runPlaybookCommand(args, options = {}) {
|
|
|
41
44
|
const env = options.env ?? process.env;
|
|
42
45
|
const cwd = options.cwd ?? process.cwd();
|
|
43
46
|
const assets = options.assets ?? resolveLauncherAssets();
|
|
47
|
+
const akariHome = env.AKARI_HOME || path.join(options.home ?? os.homedir(), '.akari');
|
|
44
48
|
if (!assets.scaffoldModulePath) throw new Error(cliText('cli.playbook.error.scaffoldMissing'));
|
|
45
49
|
const scaffold = await import(pathToFileURL(assets.scaffoldModulePath).href);
|
|
46
|
-
|
|
50
|
+
if (parsed.action === 'template') {
|
|
51
|
+
const result = await scaffold.exportPlaybookTemplate({ projectDir: path.resolve(cwd, parsed.project),
|
|
52
|
+
itemId: parsed.item, id: parsed.id, name: parsed.name, out: parsed.out ?? path.join(akariHome, 'playbooks'),
|
|
53
|
+
licenseOwn: parsed['license-own'] });
|
|
54
|
+
log(result.dir);
|
|
55
|
+
return { exitCode: 0, ...result };
|
|
56
|
+
}
|
|
47
57
|
const projectDir = parsed.into ? path.resolve(cwd, parsed.into) : null;
|
|
48
58
|
let root = null;
|
|
49
59
|
if (assets.creatorRootModulePath) {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
import { cliText } from './i18n.mjs';
|
|
7
|
+
import { resolveLauncherAssets } from './repo-assets.mjs';
|
|
8
|
+
|
|
9
|
+
const KIND_CAPABILITIES = Object.freeze({
|
|
10
|
+
image: ['image-generation'], video: ['video-generation'], music: ['music-generation'],
|
|
11
|
+
tts: ['speech-synthesis'], sns: ['publish']
|
|
12
|
+
});
|
|
13
|
+
const idPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
14
|
+
|
|
15
|
+
function parse(args) {
|
|
16
|
+
if (args.length === 0 || ['--help', '-h'].includes(args[0])) return { action: 'help' };
|
|
17
|
+
const action = args[0];
|
|
18
|
+
if (!['list', 'plan'].includes(action)) throw new Error(cliText('cli.recipe.help.usage'));
|
|
19
|
+
const id = action === 'plan' ? args[1] : null;
|
|
20
|
+
if (action === 'plan' && !idPattern.test(id ?? '')) throw new Error(cliText('cli.recipe.help.usage'));
|
|
21
|
+
const parsed = { action, id, json: false, project: null };
|
|
22
|
+
for (let i = action === 'plan' ? 2 : 1; i < args.length; i++) {
|
|
23
|
+
if (args[i] === '--json' && !parsed.json) parsed.json = true;
|
|
24
|
+
else if (args[i] === '--project' && !parsed.project && args[i + 1] && !args[i + 1].startsWith('-')) parsed.project = args[++i];
|
|
25
|
+
else throw new Error(cliText('cli.recipe.help.usage'));
|
|
26
|
+
}
|
|
27
|
+
return parsed;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function readJson(file) {
|
|
31
|
+
return JSON.parse(await fs.readFile(file, 'utf8'));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function akariHome(options) {
|
|
35
|
+
const env = options.env ?? process.env;
|
|
36
|
+
return env.AKARI_HOME || path.join(options.home ?? env.HOME ?? os.homedir(), '.akari');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function listRecipes({ projectDir, home, schemasDir }) {
|
|
40
|
+
const { validateAkariRecipe } = await import(pathToFileURL(path.join(schemasDir, 'validate-akari-recipe.mjs')).href);
|
|
41
|
+
const result = new Map();
|
|
42
|
+
for (const [source, shelf] of [['project', path.join(projectDir, 'recipes')], ['local', path.join(home, 'tool-recipes')]]) {
|
|
43
|
+
let entries;
|
|
44
|
+
try { entries = await fs.readdir(shelf, { withFileTypes: true }); }
|
|
45
|
+
catch (error) { if (error.code === 'ENOENT') continue; throw error; }
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (!idPattern.test(entry.name) || result.has(entry.name)) continue;
|
|
48
|
+
const file = path.join(shelf, entry.name, 'recipe.json');
|
|
49
|
+
let recipe;
|
|
50
|
+
try { recipe = await readJson(file); }
|
|
51
|
+
catch (error) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR' || error instanceof SyntaxError) continue; throw error; }
|
|
52
|
+
if (recipe?.kind !== 'akari-recipe' || recipe.id !== entry.name || !validateAkariRecipe(recipe).ok) continue;
|
|
53
|
+
result.set(entry.name, { id: recipe.id, name: recipe.name, needs: recipe.needs,
|
|
54
|
+
source: source === 'local' && entry.isSymbolicLink() ? 'kit' : source, recipe });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return [...result.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function candidateGates(recipe, provider) {
|
|
61
|
+
const gates = new Set(recipe.gates);
|
|
62
|
+
const billing = provider.notes?.billing ?? '';
|
|
63
|
+
const paidSignal = /(?:paid|purchase|per (?:image|minute|generation|request)|\$|usd|\u6709\u6599|\u8ab2\u91d1|\u5f93\u91cf)/iu.test(billing);
|
|
64
|
+
const freeSignal = /(?:free|no charge|\u7121\u6599|\u7121\u511f)/iu.test(billing);
|
|
65
|
+
if (billing && (paidSignal || !freeSignal)) gates.add('paid');
|
|
66
|
+
if (provider.adapter?.type === 'official-api' || provider.adapter?.type === 'custom'
|
|
67
|
+
|| (!provider.adapter && provider.auth !== 'none')) gates.add('external-send');
|
|
68
|
+
if (!provider.terms?.checked) gates.add('license');
|
|
69
|
+
return [...gates];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function planRecipe(recipe, connections) {
|
|
73
|
+
const providers = Array.isArray(connections?.providers) ? connections.providers : [];
|
|
74
|
+
const requirements = recipe.needs.map(capability => {
|
|
75
|
+
const candidates = providers.filter(provider =>
|
|
76
|
+
(provider.provides ?? KIND_CAPABILITIES[provider.kind] ?? []).includes(capability))
|
|
77
|
+
.map(provider => ({ id: provider.id, kind: provider.kind,
|
|
78
|
+
auth: provider.doctor?.status ?? 'unchecked', gates: candidateGates(recipe, provider) }));
|
|
79
|
+
return { capability, candidates, ...(candidates.length ? {} : { message: cliText('cli.recipe.plan.noCandidate') }) };
|
|
80
|
+
});
|
|
81
|
+
return { id: recipe.id, name: recipe.name, gates: recipe.gates, requirements,
|
|
82
|
+
missing: requirements.filter(item => item.candidates.length === 0).map(item => item.capability) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function runRecipeCommand(args, options = {}) {
|
|
86
|
+
const log = options.log ?? console.log;
|
|
87
|
+
const error = options.error ?? console.error;
|
|
88
|
+
try {
|
|
89
|
+
const parsed = parse(args);
|
|
90
|
+
if (parsed.action === 'help') { log(cliText('cli.recipe.help.usage')); return { exitCode: 0 }; }
|
|
91
|
+
const projectDir = path.resolve(options.cwd ?? process.cwd(), parsed.project ?? options.projectRoot ?? '.');
|
|
92
|
+
const assets = options.assets ?? resolveLauncherAssets();
|
|
93
|
+
const schemasDir = options.schemasDir ?? assets.schemasSourceDir;
|
|
94
|
+
if (!schemasDir) throw new Error(cliText('cli.recipe.error.schemasMissing'));
|
|
95
|
+
const recipes = await listRecipes({ projectDir, home: akariHome(options), schemasDir });
|
|
96
|
+
if (parsed.action === 'list') {
|
|
97
|
+
const entries = recipes.map(({ recipe, ...entry }) => entry);
|
|
98
|
+
if (parsed.json) log(JSON.stringify(entries));
|
|
99
|
+
else for (const entry of entries) log(cliText('cli.recipe.list.row', { id: entry.id, name: entry.name,
|
|
100
|
+
needs: entry.needs.join(', '), source: entry.source }));
|
|
101
|
+
return { exitCode: 0, entries };
|
|
102
|
+
}
|
|
103
|
+
const selected = recipes.find(entry => entry.id === parsed.id);
|
|
104
|
+
if (!selected) throw new Error(cliText('cli.recipe.error.notFound', { id: parsed.id }));
|
|
105
|
+
const projectConnections = path.join(projectDir, '.akari', 'connections.json');
|
|
106
|
+
let connections;
|
|
107
|
+
try { connections = await readJson(projectConnections); }
|
|
108
|
+
catch (cause) {
|
|
109
|
+
if (cause.code !== 'ENOENT') throw cause;
|
|
110
|
+
if (!assets.templateDir) throw new Error(cliText('cli.recipe.error.connectionsMissing'));
|
|
111
|
+
connections = await readJson(path.join(assets.templateDir, '.akari', 'connections.json'));
|
|
112
|
+
}
|
|
113
|
+
const plan = planRecipe(selected.recipe, connections);
|
|
114
|
+
if (parsed.json) log(JSON.stringify(plan));
|
|
115
|
+
else {
|
|
116
|
+
log(cliText('cli.recipe.plan.header', { id: plan.id, name: plan.name }));
|
|
117
|
+
if (plan.gates.length) log(cliText('cli.recipe.plan.recipeGates', { gates: plan.gates.join(', ') }));
|
|
118
|
+
for (const item of plan.requirements) {
|
|
119
|
+
if (!item.candidates.length) log(cliText('cli.recipe.plan.missingRow', { capability: item.capability, message: item.message }));
|
|
120
|
+
else for (const candidate of item.candidates) log(cliText('cli.recipe.plan.row', {
|
|
121
|
+
capability: item.capability, id: candidate.id,
|
|
122
|
+
auth: candidate.auth === 'unchecked' ? cliText('cli.recipe.auth.unchecked') : candidate.auth,
|
|
123
|
+
gates: candidate.gates.join(', ') || cliText('cli.recipe.plan.noGates') }));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { exitCode: plan.missing.length ? 1 : 0, plan };
|
|
127
|
+
} catch (cause) { error(cause.message); return { exitCode: 1 }; }
|
|
128
|
+
}
|
package/src/status-command.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { describeVersionStatus } from "./messages.mjs";
|
|
12
12
|
import { readCacheSync, readOwnVersion, resolveCachePath, resolveInstalledVersionInfo } from "./update-check.mjs";
|
|
13
13
|
import { resolveRuntimePaths } from "./runtime-diagnostics.mjs";
|
|
14
|
+
import { cliText } from "./i18n.mjs";
|
|
14
15
|
|
|
15
16
|
export async function runStatusCommand(argv, options = {}) {
|
|
16
17
|
const log = options.log ?? ((line) => process.stdout.write(line));
|
|
@@ -48,9 +49,17 @@ export async function runStatusCommand(argv, options = {}) {
|
|
|
48
49
|
|
|
49
50
|
export function formatStatusOutput(status) {
|
|
50
51
|
const summary = formatStatusSummary(status);
|
|
51
|
-
|
|
52
|
+
const activity = [];
|
|
53
|
+
if (status.automation?.action_count) activity.push(cliText("cli.status.automation.actions", {
|
|
54
|
+
count: status.automation.action_count, summary: status.automation.latest_action,
|
|
55
|
+
}));
|
|
56
|
+
if (status.automation?.pending_gate_count) activity.push(cliText("cli.status.automation.pending", {
|
|
57
|
+
count: status.automation.pending_gate_count,
|
|
58
|
+
}));
|
|
59
|
+
const output = [summary, ...activity].join("\n");
|
|
60
|
+
if (status.state_health !== "inconclusive" || status.problems.length === 0) return output;
|
|
52
61
|
const problems = status.problems.map((problem) => ` - ${problem}`).join("\n");
|
|
53
|
-
return `${
|
|
62
|
+
return `${output}\nCould not determine:\n${problems}\nNext: run \`akari status --json\` for the full machine-readable report, or resolve the items above and re-run \`akari status\`.`;
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
export function parseStatusArguments(argv, cwd = process.cwd()) {
|
|
@@ -2,6 +2,10 @@ import { lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
|
2
2
|
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
|
|
4
4
|
const FINAL_EVENT_TYPES = new Set(["final-acceptance", "final-acceptance-revoked"]);
|
|
5
|
+
const AUTOMATION_GATES = new Set([
|
|
6
|
+
"publish", "lab-submit", "paid", "external-send", "delete-original",
|
|
7
|
+
"license", "playbook-checkpoint", "human-asked",
|
|
8
|
+
]);
|
|
5
9
|
|
|
6
10
|
export function readProjectEvents(projectRoot, { gateTypes = [] } = {}) {
|
|
7
11
|
let realRoot;
|
|
@@ -147,6 +151,36 @@ export function resolveActiveAcceptance(events) {
|
|
|
147
151
|
};
|
|
148
152
|
}
|
|
149
153
|
|
|
154
|
+
/** Automation requests are separate from milestone gateTypes. Unknown events stay harmless. */
|
|
155
|
+
export function resolveAutomationSummary(events) {
|
|
156
|
+
const validBase = (payload) => payload.version === 1
|
|
157
|
+
&& isNonEmptyString(payload.id) && isNonEmptyString(payload.occurredAt);
|
|
158
|
+
const actions = events.filter(({ type, payload }) => type === "agent-action" && validBase(payload)
|
|
159
|
+
&& isNonEmptyString(payload.summary)
|
|
160
|
+
&& !/[\r\n]/u.test(payload.summary)
|
|
161
|
+
&& ["partner", "external", "unattended"].includes(payload.actor)
|
|
162
|
+
&& Array.isArray(payload.files) && payload.files.every(isNonEmptyString));
|
|
163
|
+
const resolved = new Set(events.filter(({ type, payload }) => type === "gate-resolved"
|
|
164
|
+
&& validBase(payload) && isNonEmptyString(payload.requestId)
|
|
165
|
+
&& ["approved", "declined"].includes(payload.decision) && payload.by === "human")
|
|
166
|
+
.map(({ payload }) => payload.requestId));
|
|
167
|
+
const pendingIds = new Set();
|
|
168
|
+
for (const { type, payload } of events) {
|
|
169
|
+
if (type === "gate-requested" && validBase(payload)
|
|
170
|
+
&& isNonEmptyString(payload.requestId) && isNonEmptyString(payload.summary)
|
|
171
|
+
&& AUTOMATION_GATES.has(payload.gate)
|
|
172
|
+
&& (payload.detail === undefined || typeof payload.detail === "string")
|
|
173
|
+
&& !resolved.has(payload.requestId)) {
|
|
174
|
+
pendingIds.add(payload.requestId);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
action_count: actions.length,
|
|
179
|
+
latest_action: actions.at(-1)?.payload.summary ?? null,
|
|
180
|
+
pending_gate_count: pendingIds.size,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
150
184
|
function validateFinalEventShape(payload, type) {
|
|
151
185
|
if (!FINAL_EVENT_TYPES.has(type)) return null;
|
|
152
186
|
if (!isNonEmptyString(payload.id)) return "has no id";
|
|
@@ -139,7 +139,9 @@ function validateStroke(stroke, label, problems) {
|
|
|
139
139
|
&& (!Number.isInteger(stroke.frame.cutIndex) || stroke.frame.cutIndex < 0))) {
|
|
140
140
|
problems.push(`${label}.frame is invalid for content-rect`);
|
|
141
141
|
}
|
|
142
|
-
if (!isNonEmptyString(stroke.sessionRef))
|
|
142
|
+
if (hasOwn(stroke, "sessionRef") && !isNonEmptyString(stroke.sessionRef)) {
|
|
143
|
+
problems.push(`${label}.sessionRef must be non-empty when present`);
|
|
144
|
+
}
|
|
143
145
|
} else if (hasOwn(stroke, "frame")) {
|
|
144
146
|
problems.push(`${label}.frame is forbidden for ${stroke.space}`);
|
|
145
147
|
}
|
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
|
|
5
|
-
import { readProjectEvents, resolveActiveAcceptance } from "./events.mjs";
|
|
5
|
+
import { readProjectEvents, resolveActiveAcceptance, resolveAutomationSummary } from "./events.mjs";
|
|
6
6
|
import { resolveProjectDisplayName } from "./display-name.mjs";
|
|
7
7
|
import { inspectFullIntegrity } from "./integrity.mjs";
|
|
8
8
|
import { validateAndCountReview } from "./review.mjs";
|
|
@@ -76,6 +76,7 @@ export function resolveProjectStatus(input = process.cwd(), { mode = "fast" } =
|
|
|
76
76
|
});
|
|
77
77
|
problems.push(...eventProblems);
|
|
78
78
|
const acceptanceState = resolveActiveAcceptance(events);
|
|
79
|
+
const automation = resolveAutomationSummary(events);
|
|
79
80
|
problems.push(...acceptanceState.problems);
|
|
80
81
|
|
|
81
82
|
const materials = scaffolded
|
|
@@ -120,6 +121,7 @@ export function resolveProjectStatus(input = process.cwd(), { mode = "fast" } =
|
|
|
120
121
|
waiting_on: routing.waiting_on,
|
|
121
122
|
next_skill: routing.next_skill,
|
|
122
123
|
review: reviewCounts,
|
|
124
|
+
...(automation.action_count || automation.pending_gate_count ? { automation } : {}),
|
|
123
125
|
release: { state: releaseState, accepted: false },
|
|
124
126
|
problems: [...new Set(problems)].sort((a, b) => a.localeCompare(b, "en")),
|
|
125
127
|
warnings: [...new Set(warnings)].sort((a, b) => a.localeCompare(b, "en")),
|
package/src/store-command.mjs
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
ensureKitsMarketplace,
|
|
35
35
|
linkKitAssets,
|
|
36
36
|
linkKitPlaybooks,
|
|
37
|
+
linkKitRecipes,
|
|
37
38
|
linkKitSkills,
|
|
38
39
|
readKitManifest,
|
|
39
40
|
readKitsLedger,
|
|
@@ -630,6 +631,8 @@ export async function runStoreCommand(args, options = {}) {
|
|
|
630
631
|
}
|
|
631
632
|
const playbookLinks = linkKitPlaybooks(kitDir, manifest, home, { platform: options.platform });
|
|
632
633
|
for (const warning of playbookLinks.warnings) log(cliText('cli.store.install.warning', { warning }));
|
|
634
|
+
const recipeLinks = linkKitRecipes(kitDir, manifest, home, { platform: options.platform });
|
|
635
|
+
for (const warning of recipeLinks.warnings) log(cliText('cli.store.install.warning', { warning }));
|
|
633
636
|
registerKitAssets(home, manifest, kitDir, assetLinks.items, env);
|
|
634
637
|
writeKitsLedger(home, {
|
|
635
638
|
id: manifest.id,
|
|
@@ -638,6 +641,7 @@ export async function runStoreCommand(args, options = {}) {
|
|
|
638
641
|
kitDir,
|
|
639
642
|
skills: skillLinks.linked,
|
|
640
643
|
playbooks: playbookLinks.linked,
|
|
644
|
+
recipes: recipeLinks.linked,
|
|
641
645
|
assets: assetLinks.linked
|
|
642
646
|
});
|
|
643
647
|
ensureKitsMarketplace(home);
|
|
@@ -95,6 +95,9 @@
|
|
|
95
95
|
"docs/contract-2026-10-15-script-lines-v0.md",
|
|
96
96
|
"docs/contract-2026-10-16-automation-gates-v0.md",
|
|
97
97
|
"docs/contract-2026-10-16-recipes-and-connectors-v0.md",
|
|
98
|
+
"docs/contract-2026-10-16-showcase-v0.md",
|
|
99
|
+
"docs/contract-2026-10-16-template-v0.md",
|
|
100
|
+
"docs/contract-2026-10-16-vocabulary-v0.md",
|
|
98
101
|
"packages/akari-ear/package.json",
|
|
99
102
|
"packages/akari-ear/README.md",
|
|
100
103
|
"packages/akari-launcher/package.json",
|
|
@@ -92,8 +92,10 @@
|
|
|
92
92
|
- `target` が `doc:<path>#<block-id>`、`image:<path>`、`canvas:<c-NNNN>` のときだけ
|
|
93
93
|
`sourceT:null` を許容する。動画面では従来どおり 0 以上の source 秒が必要である。
|
|
94
94
|
- `strokes[]` は `{tool:"pen", space, points, ...}` の object。`content-rect` は
|
|
95
|
-
`frame:{sourceT,cutIndex?}`
|
|
96
|
-
`
|
|
95
|
+
`frame:{sourceT,cutIndex?}` が必須。`sessionRef` は録音から作った線だけが持ち、録音なしで
|
|
96
|
+
プレビューへ直接描いた線では省略する。`image-rect` / `canvas-rect` は `frame` を持たない。
|
|
97
|
+
`content-rect` / `image-rect` の `sessionRef` と `canvas-rect` の `canvasRef` は
|
|
98
|
+
いずれも任意である。
|
|
97
99
|
|
|
98
100
|
実行可能仕様は `packages/schemas/fixtures/review/` に一元化し、`validate-review` と
|
|
99
101
|
`edit-lint` が同じ valid / invalid 全件を消費する。片方だけの専用 fixture で契約差を
|
|
@@ -73,6 +73,8 @@ review.json の data `version` は 0 のまま据え置く。compile-review-sess
|
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
- `strokeRefs` は `null`、省略、または 1 件以上の配列。欠落は従来データとして正常である。
|
|
76
|
+
- `strokeRefs` は録音の原本への参照なので、録音なしで直接描いた線には付かない。その線は
|
|
77
|
+
`strokes[].points`(描いた順)がそのまま原本になる。
|
|
76
78
|
- pen は従来どおり最大 100 点の `strokes[].points` も埋め込み、`strokeRefs` から無加工の原本へ
|
|
77
79
|
戻れる。rect は従来どおり `targetKind:"region"` + `region.box` を埋め込み、同じ `strokeRefs`
|
|
78
80
|
から `strokes.json` 内の rect 原本へ戻れる。
|
|
@@ -185,7 +185,7 @@ akari new <target-dir> --from <playbook-dir | id> [--lang <bcp47>]
|
|
|
185
185
|
|
|
186
186
|
## 8. 非スコープ
|
|
187
187
|
|
|
188
|
-
- 深さ 3(元データごと)。edit.json そのものと素材の実体の配布
|
|
188
|
+
- 深さ 3(元データごと)。edit.json そのものと素材の実体の配布 — **ただし「まとまり 1 つ + その素材」の深さ 3 = テンプレは `contract-2026-10-16-template-v0.md` で範囲内にした(2026-10-16)**
|
|
189
189
|
- Lab での公開・入手・検索・タグ俯瞰・クリエイター登録・還元
|
|
190
190
|
- URL を貼って作り方を起こす分析
|
|
191
191
|
- 画面の最終デザイン(ポップアップの出だしは別の実装票。本契約は形と CLI と確認シートまで)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
lifecycle:
|
|
2
|
+
lifecycle: accepted
|
|
3
3
|
created: 2026-10-16
|
|
4
4
|
updated: 2026-10-16
|
|
5
5
|
---
|
|
@@ -7,7 +7,7 @@ updated: 2026-10-16
|
|
|
7
7
|
# recipes-and-connectors-v0(レシピと接続の 2 層)契約
|
|
8
8
|
|
|
9
9
|
- 日付: 2026-10-16
|
|
10
|
-
- 状態:
|
|
10
|
+
- 状態: **accepted**(2026-10-16。語と置き場は §6 の既定で確定。語は後から改名できるよう、ファイルの見分けは `kind` で行う)
|
|
11
11
|
- 前提: `contract-2026-07-14-3d-bake-recipe.md`(道具のレシピ = 正本、焼いた出力 = 再生成できるキャッシュ)/
|
|
12
12
|
`contract-2026-09-13-extension-kit-v0.md`(キットの manifest。v0 の `kind` は `kit` のみ)/
|
|
13
13
|
`packages/schemas/connections.schema.json`(接続の台帳。`kind`: genai / image / video / tts / music / sns / analytics / notify)/
|
|
@@ -73,10 +73,17 @@ updated: 2026-10-16
|
|
|
73
73
|
- 公式の API が無いサービスを非公式に叩くアダプタは**同梱しない**。配る場合は個別に審査する(相手の規約に触れうる)
|
|
74
74
|
- 有償・外部送信は門(`paid` / `external-send`)。どのおまかせ度でも止まる
|
|
75
75
|
- MCP サーバーは作らない(2 種類目のエージェントか外部アプリが要ったとき)。外から叩く口はファイル契約と `akari` の副コマンド
|
|
76
|
-
- 個々のアダプタの実装・レシピの GUI
|
|
76
|
+
- 個々のアダプタの実装・レシピの GUI・レシピの実行エンジン(実行はエージェント)
|
|
77
77
|
|
|
78
|
-
## 6.
|
|
78
|
+
## 6. 語と置き場(2026-10-16 の既定)
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
- 画面の語: シチュエーションの層を **レシピ** と呼ぶ。既存の `contract-2026-07-25-recipe-v0.md` の `recipe.json`
|
|
81
|
+
(確認済みの好みの凍結。`~/.akari/recipes/<name>.json`)は**そのまま**で、画面では「好みの記憶」と呼ぶ。2 つはファイルの `kind` で見分ける
|
|
82
|
+
(本契約の宣言は `"kind": "akari-recipe"`。好みの凍結は `kind` を持たない)
|
|
83
|
+
- 置き場(作り方と同じ形の棚): 手元 = `~/.akari/tool-recipes/<id>/recipe.json`(`~/.akari/recipes/` には置かない)/
|
|
84
|
+
キット = `recipes/<id>/recipe.json`(キットの manifest に `recipes: [{ "dir": "recipes/<id>" }]`。`akari store install` で手元の棚へ symlink)/
|
|
85
|
+
プロジェクト = `<project>/recipes/<id>/recipe.json`(そのプロジェクトだけで使うもの)。同じ id はプロジェクト → 手元 → キットの順で先を使う
|
|
86
|
+
- 動詞(CLI): `akari recipe list [--json]`(棚の一覧)/ `akari recipe plan <id> [--project <dir>]`(要る能力 → 使える接続の候補・
|
|
87
|
+
止まる門〈`paid` / `external-send` / `terms.checked` が無ければ `license`〉を出す。**実行はしない** — 実行はエージェントがスキルと既存の動詞
|
|
88
|
+
〈`akari generate` など〉で行い、出力を manifest の tool-output に recipe / connector つきで載せる)
|
|
89
|
+
- 改名するときは、画面の語と CLI の別名を足すだけで、ファイルの `kind` と置き場は変えない
|