@mawaru/sdk 0.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.
Files changed (43) hide show
  1. package/_schemas/ai-manifest.test.ts +135 -0
  2. package/_schemas/ai-manifest.ts +76 -0
  3. package/_schemas/graph.test.ts +872 -0
  4. package/_schemas/graph.ts +439 -0
  5. package/_schemas/hook-manifest.test.ts +45 -0
  6. package/_schemas/hook-manifest.ts +25 -0
  7. package/_schemas/node.test.ts +76 -0
  8. package/_schemas/node.ts +54 -0
  9. package/_schemas/port-spec.test.ts +657 -0
  10. package/_schemas/port-spec.ts +405 -0
  11. package/_schemas/program-manifest.test.ts +126 -0
  12. package/_schemas/program-manifest.ts +43 -0
  13. package/dist/_schemas/ai-manifest.d.ts +25 -0
  14. package/dist/_schemas/ai-manifest.js +67 -0
  15. package/dist/_schemas/graph.d.ts +226 -0
  16. package/dist/_schemas/graph.js +372 -0
  17. package/dist/_schemas/hook-manifest.d.ts +18 -0
  18. package/dist/_schemas/hook-manifest.js +21 -0
  19. package/dist/_schemas/node.d.ts +47 -0
  20. package/dist/_schemas/node.js +42 -0
  21. package/dist/_schemas/port-spec.d.ts +62 -0
  22. package/dist/_schemas/port-spec.js +336 -0
  23. package/dist/_schemas/program-manifest.d.ts +10 -0
  24. package/dist/_schemas/program-manifest.js +41 -0
  25. package/dist/cli.d.ts +2 -0
  26. package/dist/cli.js +54 -0
  27. package/dist/index.d.ts +6 -0
  28. package/dist/index.js +9 -0
  29. package/dist/init.d.ts +6 -0
  30. package/dist/init.js +81 -0
  31. package/dist/typegen.d.ts +14 -0
  32. package/dist/typegen.js +206 -0
  33. package/dist/validate.d.ts +6 -0
  34. package/dist/validate.js +130 -0
  35. package/docs/development.md +59 -0
  36. package/index.ts +9 -0
  37. package/package.json +47 -0
  38. package/skills/create-loop/SKILL.md +87 -0
  39. package/templates/CLAUDE.md +23 -0
  40. package/templates/README.md +12 -0
  41. package/templates/echo/config.json +23 -0
  42. package/templates/echo/main.ts +4 -0
  43. package/templates/mawaru-runner.yml +71 -0
package/dist/init.js ADDED
@@ -0,0 +1,81 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ // このファイルは dev では src/sdk/init.ts・配布物では dist/init.js に居る。
5
+ // templates/ 等は package root 直下なので、package.json まで遡って解決する
6
+ const packageRoot = () => {
7
+ let dir = dirname(fileURLToPath(import.meta.url));
8
+ while (!existsSync(join(dir, "package.json"))) {
9
+ const parent = dirname(dir);
10
+ if (parent === dir) {
11
+ throw new Error("@mawaru/sdk の package root が見つかりません");
12
+ }
13
+ dir = parent;
14
+ }
15
+ return dir;
16
+ };
17
+ const template = (name) => readFileSync(join(packageRoot(), "templates", name), "utf-8");
18
+ // スキルポインタ:本体(skills/<name>/SKILL.md)の frontmatter を写して Claude Code に
19
+ // 発見させ、中身は node_modules 内の本体へ誘導する(repo に焼くものは最小・本体は npm で解決)
20
+ const skillPointer = (name, body) => {
21
+ const m = body.match(/^---\n[\s\S]*?\n---\n/);
22
+ const frontmatter = m?.[0];
23
+ if (!frontmatter) {
24
+ throw new Error(`skills/${name}/SKILL.md に frontmatter がありません`);
25
+ }
26
+ return `${frontmatter}
27
+ このスキルの本体は \`node_modules/@mawaru/sdk/skills/${name}/SKILL.md\` にある
28
+ (\`npx @mawaru/sdk init\` が生成するポインタ。本体は npm パッケージの更新で配布される)。
29
+ **必ず本体を読み、その手順に従うこと。**
30
+ `;
31
+ };
32
+ export const runInit = (root) => {
33
+ const written = [];
34
+ const skipped = [];
35
+ const notes = [];
36
+ const write = (path, content) => {
37
+ const abs = join(root, path);
38
+ mkdirSync(dirname(abs), { recursive: true });
39
+ writeFileSync(abs, content);
40
+ written.push(path);
41
+ };
42
+ // ユーザーが育てるファイル:無ければ生成・あれば触らない
43
+ const writeIfAbsent = (path, content) => {
44
+ if (existsSync(join(root, path))) {
45
+ skipped.push(path);
46
+ return;
47
+ }
48
+ write(path, content);
49
+ };
50
+ // mawaru 所有:毎回テンプレートで上書き
51
+ write(".github/workflows/mawaru-runner.yml", template("mawaru-runner.yml"));
52
+ // nodes/ の骨組みとサンプル
53
+ writeIfAbsent("nodes/program/echo/config.json", template("echo/config.json"));
54
+ writeIfAbsent("nodes/program/echo/main.ts", template("echo/main.ts"));
55
+ writeIfAbsent("nodes/ai/.gitkeep", "");
56
+ // AI 開発の足場(スキルポインタは mawaru 所有=毎回上書き。SDK にスキルが増えたら
57
+ // init 再実行でポインタも増える)
58
+ const skillsDir = join(packageRoot(), "skills");
59
+ for (const name of readdirSync(skillsDir)) {
60
+ const bodyPath = join(skillsDir, name, "SKILL.md");
61
+ if (!existsSync(bodyPath))
62
+ continue;
63
+ write(`.claude/skills/${name}/SKILL.md`, skillPointer(name, readFileSync(bodyPath, "utf-8")));
64
+ }
65
+ writeIfAbsent("CLAUDE.md", template("CLAUDE.md"));
66
+ writeIfAbsent("README.md", template("README.md"));
67
+ // package.json:@mawaru/sdk を devDependencies に(ポインタ・docs の参照解決と
68
+ // typegen / validate CLI のため)。バージョンはこのパッケージ自身から取る
69
+ const sdkVersion = JSON.parse(readFileSync(join(packageRoot(), "package.json"), "utf-8")).version;
70
+ writeIfAbsent("package.json", `${JSON.stringify({
71
+ name: "mawaru",
72
+ private: true,
73
+ type: "module",
74
+ devDependencies: { "@mawaru/sdk": `^${sdkVersion}` },
75
+ }, null, 2)}\n`);
76
+ // runner yml の `npm ci` とポインタの参照解決は node_modules / lockfile が前提
77
+ if (!existsSync(join(root, "package-lock.json"))) {
78
+ notes.push("npm install を実行して package-lock.json を commit してください(runner の npm ci と node_modules 参照に必要です)");
79
+ }
80
+ return { written, skipped, notes };
81
+ };
@@ -0,0 +1,14 @@
1
+ import type { JsonSchema } from "./_schemas/node.js";
2
+ import { type ProgramManifest } from "./_schemas/program-manifest.js";
3
+ export declare const CONFIG_HASH_PREFIX = "// mawaru:config-hash=";
4
+ export declare const configHash: (rawConfig: string) => string;
5
+ export declare const tsTypeFromJsonSchema: (schema: JsonSchema, indent?: number) => string;
6
+ export declare const renderTypesFile: (manifest: ProgramManifest, rawConfig: string) => string;
7
+ export type TypegenResult = {
8
+ written: string[];
9
+ errors: {
10
+ dir: string;
11
+ message: string;
12
+ }[];
13
+ };
14
+ export declare const runTypegen: (root: string) => TypegenResult;
@@ -0,0 +1,206 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { programManifestSchema, } from "./_schemas/program-manifest.js";
5
+ // 生成ファイルに刻む config.json 内容のハッシュ。validate が drift(config を変えて
6
+ // typegen し忘れ)の検出に使う
7
+ export const CONFIG_HASH_PREFIX = "// mawaru:config-hash=";
8
+ export const configHash = (rawConfig) => createHash("sha256").update(rawConfig).digest("hex").slice(0, 16);
9
+ const IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
10
+ const propertyKey = (key) => IDENTIFIER_RE.test(key) ? key : JSON.stringify(key);
11
+ const literalType = (value) => value === null ? "null" : (JSON.stringify(value) ?? "unknown");
12
+ // union / tuple の要素は結合子と混ざらないよう括弧で包む
13
+ const wrapIfUnion = (type) => type.includes(" | ") || type.includes(" & ") ? `(${type})` : type;
14
+ const objectType = (schema, indent) => {
15
+ const properties = schema.properties;
16
+ if (typeof properties !== "object" ||
17
+ properties === null ||
18
+ Array.isArray(properties)) {
19
+ const additional = schema.additionalProperties;
20
+ if (typeof additional === "object" && additional !== null) {
21
+ return `Record<string, ${tsTypeFromJsonSchema(additional, indent)}>`;
22
+ }
23
+ if (additional === false) {
24
+ return "Record<string, never>";
25
+ }
26
+ return "Record<string, unknown>";
27
+ }
28
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
29
+ const inner = " ".repeat(indent + 1);
30
+ const lines = [];
31
+ for (const [key, propSchema] of Object.entries(properties)) {
32
+ const description = typeof propSchema === "object" &&
33
+ propSchema !== null &&
34
+ !Array.isArray(propSchema)
35
+ ? propSchema.description
36
+ : undefined;
37
+ if (typeof description === "string" && description.length > 0) {
38
+ lines.push(`${inner}/** ${description} */`);
39
+ }
40
+ const optional = required.has(key) ? "" : "?";
41
+ lines.push(`${inner}${propertyKey(key)}${optional}: ${tsTypeFromJsonSchema(propSchema, indent + 1)}`);
42
+ }
43
+ if (lines.length === 0) {
44
+ return "Record<string, unknown>";
45
+ }
46
+ return `{\n${lines.join("\n")}\n${" ".repeat(indent)}}`;
47
+ };
48
+ const namedType = (typeName, schema, indent) => {
49
+ switch (typeName) {
50
+ case "string":
51
+ return "string";
52
+ case "number":
53
+ case "integer":
54
+ return "number";
55
+ case "boolean":
56
+ return "boolean";
57
+ case "null":
58
+ return "null";
59
+ case "array": {
60
+ const items = schema.items;
61
+ if (Array.isArray(items)) {
62
+ return `[${items.map((i) => tsTypeFromJsonSchema(i, indent)).join(", ")}]`;
63
+ }
64
+ if (typeof items === "object" && items !== null) {
65
+ return `${wrapIfUnion(tsTypeFromJsonSchema(items, indent))}[]`;
66
+ }
67
+ return "unknown[]";
68
+ }
69
+ case "object":
70
+ return objectType(schema, indent);
71
+ default:
72
+ return "unknown";
73
+ }
74
+ };
75
+ // JSON Schema(zodFromJsonSchema が扱うサブセット)を TS 型の文字列に写す。
76
+ // 解釈できない形は unknown に落とす(型が付かないだけで、実行時検証は ports 側が担う)
77
+ export const tsTypeFromJsonSchema = (schema, indent = 0) => {
78
+ if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
79
+ return "unknown";
80
+ }
81
+ if ("const" in schema) {
82
+ return literalType(schema.const);
83
+ }
84
+ if (Array.isArray(schema.enum)) {
85
+ return schema.enum.length > 0
86
+ ? schema.enum.map(literalType).join(" | ")
87
+ : "unknown";
88
+ }
89
+ const variants = schema.oneOf ?? schema.anyOf;
90
+ if (Array.isArray(variants) && variants.length > 0) {
91
+ return variants
92
+ .map((v) => tsTypeFromJsonSchema(v, indent))
93
+ .join(" | ");
94
+ }
95
+ if (Array.isArray(schema.allOf) && schema.allOf.length > 0) {
96
+ return schema.allOf
97
+ .map((v) => wrapIfUnion(tsTypeFromJsonSchema(v, indent)))
98
+ .join(" & ");
99
+ }
100
+ const type = schema.type;
101
+ if (Array.isArray(type)) {
102
+ return type.length > 0
103
+ ? type.map((t) => namedType(String(t), schema, indent)).join(" | ")
104
+ : "unknown";
105
+ }
106
+ if (typeof type === "string") {
107
+ return namedType(type, schema, indent);
108
+ }
109
+ return "unknown";
110
+ };
111
+ // Output(run の戻り値)=「ちょうど1つの出口ポートのキーで返す封筒」の union。
112
+ // 他ポートのキーを ?: never で締めるのは excess property check の穴
113
+ // (union の他メンバーにあるキーは許される)を塞ぐため。複数キー封筒は
114
+ // エンジン(resolveOutputEnvelope)が実行時に拒否する契約で、それを tsc に前倒しする
115
+ const envelopeUnionType = (outputKeys) => {
116
+ const member = (key) => {
117
+ const props = [
118
+ `${propertyKey(key)}: Outputs[${JSON.stringify(key)}]`,
119
+ ...outputKeys
120
+ .filter((k) => k !== key)
121
+ .map((k) => `${propertyKey(k)}?: never`),
122
+ ];
123
+ return `{ ${props.join("; ")} }`;
124
+ };
125
+ const first = outputKeys[0];
126
+ if (outputKeys.length === 1 && first !== undefined) {
127
+ return `export type Output = ${member(first)}`;
128
+ }
129
+ return `export type Output =\n${outputKeys.map((k) => ` | ${member(k)}`).join("\n")}`;
130
+ };
131
+ export const renderTypesFile = (manifest, rawConfig) => {
132
+ const inputSchema = manifest.inputs.in ?? {};
133
+ const outputEntries = Object.entries(manifest.outputs).map(([key, schema]) => ` ${propertyKey(key)}: ${tsTypeFromJsonSchema(schema, 1)}`);
134
+ // refs=ループ変数の袋(実行時に refs.json で渡される)。config の refs はループ定義
135
+ // からの転写(キー→推定型)なので、宣言が無ければ型も出さない
136
+ const refEntries = Object.entries(manifest.refs).map(([key, schema]) => ` ${propertyKey(key)}: ${tsTypeFromJsonSchema(schema, 1)}`);
137
+ return [
138
+ "// このファイルは mawaru typegen が config.json から自動生成した(手で編集しない)",
139
+ `${CONFIG_HASH_PREFIX}${configHash(rawConfig)}`,
140
+ "",
141
+ `export type Input = ${tsTypeFromJsonSchema(inputSchema)}`,
142
+ "",
143
+ `export type Outputs = {\n${outputEntries.join("\n")}\n}`,
144
+ "",
145
+ "// run の戻り値:ちょうど1つの出口ポートのキーで返す封筒",
146
+ envelopeUnionType(Object.keys(manifest.outputs)),
147
+ "",
148
+ ...(refEntries.length > 0
149
+ ? [`export type Refs = {\n${refEntries.join("\n")}\n}`, ""]
150
+ : []),
151
+ ].join("\n");
152
+ };
153
+ // nodes/program/ 直下の各 dir の config.json から types.d.ts を生成する
154
+ export const runTypegen = (root) => {
155
+ const programsDir = join(root, "nodes", "program");
156
+ if (!existsSync(programsDir)) {
157
+ return {
158
+ written: [],
159
+ errors: [
160
+ {
161
+ dir: "nodes/program",
162
+ message: "nodes/program/ ディレクトリがありません",
163
+ },
164
+ ],
165
+ };
166
+ }
167
+ const written = [];
168
+ const errors = [];
169
+ for (const entry of readdirSync(programsDir, { withFileTypes: true })) {
170
+ if (!entry.isDirectory()) {
171
+ continue;
172
+ }
173
+ const configPath = join(programsDir, entry.name, "config.json");
174
+ if (!existsSync(configPath)) {
175
+ errors.push({ dir: entry.name, message: "config.json がありません" });
176
+ continue;
177
+ }
178
+ const rawConfig = readFileSync(configPath, "utf8");
179
+ let parsed;
180
+ try {
181
+ parsed = JSON.parse(rawConfig);
182
+ }
183
+ catch {
184
+ errors.push({
185
+ dir: entry.name,
186
+ message: "config.json が JSON として不正です",
187
+ });
188
+ continue;
189
+ }
190
+ const manifest = programManifestSchema.safeParse(parsed);
191
+ if (!manifest.success) {
192
+ const detail = manifest.error.issues
193
+ .map((i) => i.path.length > 0 ? `${i.path.join(".")}: ${i.message}` : i.message)
194
+ .join(" / ");
195
+ errors.push({
196
+ dir: entry.name,
197
+ message: `config.json が規約に合いません(${detail})`,
198
+ });
199
+ continue;
200
+ }
201
+ const typesPath = join(programsDir, entry.name, "types.d.ts");
202
+ writeFileSync(typesPath, renderTypesFile(manifest.data, rawConfig));
203
+ written.push(typesPath);
204
+ }
205
+ return { written, errors };
206
+ };
@@ -0,0 +1,6 @@
1
+ export type Issue = {
2
+ level: "error" | "warning";
3
+ path: string;
4
+ message: string;
5
+ };
6
+ export declare const validateRepo: (root: string) => Issue[];
@@ -0,0 +1,130 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ import { z } from "zod";
4
+ import { convertJsonSchemaToZod } from "zod-from-json-schema";
5
+ import { programManifestSchema } from "./_schemas/program-manifest.js";
6
+ import { CONFIG_HASH_PREFIX, configHash } from "./typegen.js";
7
+ // エディタ・オーケストレータは zod-from-json-schema(common の zodFromJsonSchema)で
8
+ // スキーマを解釈する。ここで通らないスキーマは実行時にも使えない
9
+ const checkPortSchemas = (ports, group, path, issues) => {
10
+ for (const [key, schema] of Object.entries(ports)) {
11
+ try {
12
+ // 未知の type は例外でなく ZodNever(どんな値も受け付けない)に変換されるので、それも弾く
13
+ if (convertJsonSchemaToZod(schema) instanceof z.ZodNever) {
14
+ issues.push({
15
+ level: "error",
16
+ path,
17
+ message: `${group}.${key} のスキーマがどんな値も受け付けません(type の綴りを確認してください)`,
18
+ });
19
+ }
20
+ }
21
+ catch (e) {
22
+ issues.push({
23
+ level: "error",
24
+ path,
25
+ message: `${group}.${key} のスキーマを解釈できません(${e instanceof Error ? e.message : e})`,
26
+ });
27
+ }
28
+ }
29
+ };
30
+ const checkTypegenDrift = (programDir, rawConfig, path, issues) => {
31
+ const typesPath = join(programDir, "types.d.ts");
32
+ if (!existsSync(typesPath)) {
33
+ issues.push({
34
+ level: "warning",
35
+ path,
36
+ message: "types.d.ts がありません(mawaru typegen で生成すると main.ts に型が付きます)",
37
+ });
38
+ return;
39
+ }
40
+ const stampLine = readFileSync(typesPath, "utf8")
41
+ .split("\n")
42
+ .find((line) => line.startsWith(CONFIG_HASH_PREFIX));
43
+ if (!stampLine) {
44
+ issues.push({
45
+ level: "warning",
46
+ path: `${path}/types.d.ts`,
47
+ message: "typegen 管理外の types.d.ts です(ハッシュスタンプがありません)",
48
+ });
49
+ return;
50
+ }
51
+ if (stampLine.slice(CONFIG_HASH_PREFIX.length) !== configHash(rawConfig)) {
52
+ issues.push({
53
+ level: "error",
54
+ path: `${path}/types.d.ts`,
55
+ message: "config.json と乖離しています(mawaru typegen を再実行してください)",
56
+ });
57
+ }
58
+ };
59
+ const validateProgram = (programsDir, dir, root) => {
60
+ const issues = [];
61
+ const programDir = join(programsDir, dir);
62
+ const path = relative(root, programDir);
63
+ const configPath = join(programDir, "config.json");
64
+ if (!existsSync(configPath)) {
65
+ issues.push({ level: "error", path, message: "config.json がありません" });
66
+ return issues;
67
+ }
68
+ if (!existsSync(join(programDir, "main.ts"))) {
69
+ issues.push({ level: "error", path, message: "main.ts がありません" });
70
+ }
71
+ const rawConfig = readFileSync(configPath, "utf8");
72
+ let parsed;
73
+ try {
74
+ parsed = JSON.parse(rawConfig);
75
+ }
76
+ catch {
77
+ issues.push({
78
+ level: "error",
79
+ path: `${path}/config.json`,
80
+ message: "JSON として不正です",
81
+ });
82
+ return issues;
83
+ }
84
+ const manifest = programManifestSchema.safeParse(parsed);
85
+ if (!manifest.success) {
86
+ for (const issue of manifest.error.issues) {
87
+ const at = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
88
+ issues.push({
89
+ level: "error",
90
+ path: `${path}/config.json`,
91
+ message: `${at}${issue.message}`,
92
+ });
93
+ }
94
+ return issues;
95
+ }
96
+ checkPortSchemas(manifest.data.inputs, "inputs", `${path}/config.json`, issues);
97
+ checkPortSchemas(manifest.data.outputs, "outputs", `${path}/config.json`, issues);
98
+ // refs はループ定義からの転写(生成物)だが、手編集や転写前の古い形が
99
+ // 残っている可能性があるので inputs / outputs と同じ基準で検査する
100
+ checkPortSchemas(manifest.data.refs, "refs", `${path}/config.json`, issues);
101
+ checkTypegenDrift(programDir, rawConfig, path, issues);
102
+ return issues;
103
+ };
104
+ // リポジトリ規約の構造検証。error が1つでもあれば CI を落とす想定(cli.ts が exit 1)
105
+ export const validateRepo = (root) => {
106
+ const issues = [];
107
+ const programsDir = join(root, "nodes", "program");
108
+ if (!existsSync(programsDir)) {
109
+ issues.push({
110
+ level: "warning",
111
+ path: ".",
112
+ message: "nodes/program/ ディレクトリがありません",
113
+ });
114
+ }
115
+ else {
116
+ for (const entry of readdirSync(programsDir, { withFileTypes: true })) {
117
+ if (entry.isDirectory()) {
118
+ issues.push(...validateProgram(programsDir, entry.name, root));
119
+ }
120
+ }
121
+ }
122
+ if (!existsSync(join(root, ".github", "workflows", "mawaru-runner.yml"))) {
123
+ issues.push({
124
+ level: "warning",
125
+ path: ".github/workflows",
126
+ message: "mawaru-runner.yml がありません(Actions 実行に必要です)",
127
+ });
128
+ }
129
+ return issues;
130
+ };
@@ -0,0 +1,59 @@
1
+ # mawaru 実行 repo の loop / node 開発ガイド
2
+
3
+ mawaru の実行 repo(`npx @mawaru/sdk init` で初期化された repo)で Program / AI ノードと
4
+ loop(回路)を開発するためのガイド。**契約の正はこのパッケージの `_schemas/`(Zod スキーマ。
5
+ 日本語コメント付き)**。この文書と食い違ったらスキーマが正。
6
+
7
+ ## repo の位置づけ
8
+
9
+ - この repo に置くのは**ユーザーのカスタマイズ**(ノードの実装・定義)とその履歴だけ
10
+ - 実行基盤の実態(runner・ラッパー)は npm パッケージ(`@mawaru/agent-runner`)にあり、
11
+ mawaru が dispatch payload でバージョンを指定して実行する。repo 側の更新は不要
12
+ - `.github/workflows/mawaru-runner.yml` は mawaru 所有の薄いシム。**編集しない**
13
+ (改訂は `npx @mawaru/sdk init` の再実行で配布される)
14
+
15
+ ## ディレクトリ規約
16
+
17
+ ```
18
+ nodes/
19
+ program/<dir>/ # Program ノード:config.json + main.ts(+任意で package.json)
20
+ ai/<dir>/ # AI ノード定義:config.json
21
+ hook/<dir>/ # NodeIOHook:config.json + main.ts(ノードに付く観測フック)
22
+ skills/<dir>/ # AI ノードが参照する skill(SKILL.md)
23
+ ```
24
+
25
+ ## Program ノード(`nodes/program/<dir>/`)
26
+
27
+ - `config.json` — 入出力スキーマ等の宣言。正は `_schemas/program-manifest.ts`
28
+ (`inputs` は `"in"` の1件だけ・`outputs` は1件以上・`env` は必要な secret 名の宣言)
29
+ - `main.ts` — `run(input, ctx)` を export する。`ctx.refs` にループ変数が入る。戻り値:
30
+ - `{ <出口ポートのkey>: <データ> }` の**単一キー封筒**(どの出口から出るかを戻り値自身が運ぶ)
31
+ - `"pending"` — 完了保留(外部イベントで後から end が届く)
32
+ - 型生成:`npx mawaru typegen` が config.json から `types.d.ts`(Input / Outputs 型)を生成する
33
+ - **ノード単位の npm 依存**:`<dir>/package.json` を置けば実行前にそのディレクトリで
34
+ `npm ci` される。`package-lock.json` の commit が必須(無いと明瞭に失敗する)
35
+ - 環境変数:`config.json` の `env` に宣言した secret(repo の Actions secrets)だけが
36
+ 実行時に渡される(最小権限)
37
+
38
+ ## AI ノード(`nodes/ai/<dir>/`)
39
+
40
+ - `config.json` — 正は `_schemas/ai-manifest.ts`。`prompt`(必須)・`model`・
41
+ `skills`(トップレベル `skills/` 配下のディレクトリ名)・`env`・`inputs` / `outputs`
42
+ - 実行時はラッパーが Claude Code(headless)を起動し、`outputs` のスキーマに適合する
43
+ output を検証して報告する。出口が複数あれば AI が1つ選ぶ(複数出口=分岐)
44
+
45
+ ## フック(`nodes/hook/<dir>/`)
46
+
47
+ - ノードの入出力・外部シグナルを観測する独立実行。正は `_schemas/hook-manifest.ts`
48
+ (`on`: input / output / signal の宣言・`env`)
49
+
50
+ ## loop(回路)の作成・更新
51
+
52
+ - `.claude/skills/create-loop` スキルに従う(graph API の契約は `_schemas/graph.ts` が正)
53
+ - ノードのポートは repo の config.json から取り込まれる(repo が正・エディタ保存時に
54
+ 書き戻し同期される)
55
+
56
+ ## 検証
57
+
58
+ - `npx mawaru validate` — リポジトリ規約の検証(CI 向け。error で exit 1)
59
+ - config.json を変えたら `npx mawaru typegen` で型を更新し、ズレをコンパイルで検出する
package/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ // mawaru の契約スキーマ(実行 repo の config.json 規約+ループ graph API のボディ)。
2
+ // 正はここ(npm 公開)で、@mawaru/common(private)が re-export して
3
+ // backend / frontend が使う。CLI(typegen / validate)の実装は export しない
4
+ export * from "./_schemas/ai-manifest.js"
5
+ export * from "./_schemas/graph.js"
6
+ export * from "./_schemas/hook-manifest.js"
7
+ export * from "./_schemas/node.js"
8
+ export * from "./_schemas/port-spec.js"
9
+ export * from "./_schemas/program-manifest.js"
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@mawaru/sdk",
3
+ "version": "0.5.0",
4
+ "description": "mawaru 実行 repo の開発 SDK。契約スキーマ(config.json 規約・ループ graph API)の正 + typegen / validate CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "mawaru": "dist/cli.js"
8
+ },
9
+ "exports": {
10
+ ".": "./index.ts"
11
+ },
12
+ "publishConfig": {
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "skills",
23
+ "templates",
24
+ "docs",
25
+ "_schemas",
26
+ "index.ts"
27
+ ],
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.build.json",
33
+ "prepack": "pnpm build",
34
+ "typecheck": "tsc --noEmit",
35
+ "test": "vitest run",
36
+ "test:watch": "vitest"
37
+ },
38
+ "dependencies": {
39
+ "zod": "^4.4.3",
40
+ "zod-from-json-schema": "^0.5.3"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^26.1.0",
44
+ "typescript": "^6.0.3",
45
+ "vitest": "^4.1.9"
46
+ }
47
+ }
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: create-loop
3
+ description: mawaru のループ(回路)を API 経由で作成・更新するスキル。「◯◯するループを作って」「mawaru にループを定義して」「このプログラムをループにつないで」などのリクエストで発動する。実行 repo(nodes/program・nodes/ai を持つリポジトリ)の中で使う。
4
+ ---
5
+
6
+ # create-loop — mawaru のループを API で作成する
7
+
8
+ mawaru のループ(Program / AI / Human / Wait ノードを connection でつないだ回路)を、
9
+ エディタを使わず REST API で作成・更新する手順。
10
+
11
+ ## セットアップ(初回のみ。揃っていればスキップ)
12
+
13
+ repo ルートの `.env` に以下の4変数が揃っているか確認する。足りなければ**このスキルの中で
14
+ セットアップまで面倒を見る**(ユーザーに手作業のセットアップを求めない):
15
+
16
+ 1. `.gitignore` に `.env` が入っていることを確認する。無ければ追加する(API キーをコミットさせない)。
17
+ 2. `.env` を作成・追記する:
18
+ - `MAWARU_API_URL` — API のベース URL(例: `https://api.mawaru.ai`。ローカル開発は `http://localhost:9000`)
19
+ - `MAWARU_APP_URL` — フロントの URL(例: `https://app.mawaru.ai`。ローカル開発は `http://localhost:3000`)
20
+ - `MAWARU_TENANT_ID` — tenant の ID
21
+ - `MAWARU_API_KEY` — tenant の API キー
22
+ 接続先が本番かローカルかはユーザーに確認する。API キーは mawaru のテナント設定画面
23
+ (`{MAWARU_APP_URL}/settings` の「API キー」。admin のみ)で発行・コピーできるので、
24
+ ユーザーに案内して値を入力してもらう。**キーの値をチャット・ログ・コミットに出さない。**
25
+ 3. 疎通確認:`GET {MAWARU_API_URL}/tenants/{MAWARU_TENANT_ID}/loops` が 200 を返せば完了。
26
+ 401 なら API キー、404 なら tenant ID を疑う。
27
+
28
+ 認証は全リクエスト共通でヘッダ `x-api-key: $MAWARU_API_KEY`。
29
+
30
+ ## 契約の読み方(最重要)
31
+
32
+ **API のボディ形をこの文書は説明しない。正はこのパッケージ内の Zod スキーマなので、必ずソースを読むこと**:
33
+
34
+ - `node_modules/@mawaru/sdk/_schemas/graph.ts` — graph 保存ボディ(`saveGraphBodySchema`)。
35
+ ノード・接続・refs(ループ変数)・hooks の形と、superRefine 内の全整合性ルール(日本語コメント付き)
36
+ - `node_modules/@mawaru/sdk/_schemas/node.ts` — ポートの形(`graphPortSchema`)と key の規則
37
+ - `node_modules/@mawaru/sdk/_schemas/port-spec.ts` — **kind 別の必須ポート構成**(`kindPortsViolation`)と
38
+ 接続の型規則(human の in はカタログ型 envelope、program の in はスキーマ包含 等)
39
+ - `node_modules/@mawaru/sdk/_schemas/program-manifest.ts` / `ai-manifest.ts` — repo 側 config.json の規約
40
+ - 同ディレクトリの `*.test.ts` — 通る graph / 弾かれる graph の実例集として読める
41
+
42
+ ## API(パスとボディは openapi.json が正)
43
+
44
+ **パス・メソッド・リクエストボディを手書きで写さない。サーバが自己記述する OpenAPI を読む**:
45
+
46
+ 疎通確認のあと `GET {MAWARU_API_URL}/tenants/{MAWARU_TENANT_ID}/openapi.json`
47
+ (ヘッダ `x-api-key: $MAWARU_API_KEY`)を取得すると、機械 API 面
48
+ (loops の一覧・作成・詳細・graph PUT、runs/start、members、api-keys、steps の end / decision)の
49
+ パス・メソッド・リクエストボディ(Zod 由来の JSON Schema)が載っている。ここが常に正。
50
+ 段階導入中で未掲載のエンドポイントもある(その場合だけ `api/index.ts` 相当を辿る)。
51
+
52
+ すべて `{MAWARU_API_URL}/tenants/{MAWARU_TENANT_ID}/` 配下。openapi.json のボディ schema は
53
+ 形(キー・型)まで。graph の詳細な整合性ルール(superRefine)は上の Zod ソースが正なので
54
+ 必ず併読すること。
55
+
56
+ エラーは正直に返る:ボディ形の違反は 422(zod の issue が日本語で入っている)、意味の違反も 422、
57
+ repo との矛盾・使用中ノードの削除は 409。**graph PUT は冪等な全量置換なので、
58
+ エラーメッセージを読んで直して再送を繰り返せばよい。**
59
+
60
+ ## 手順
61
+
62
+ 1. **設計**:要件から必要なノード(program / ai / human / wait)と流れを決め、ユーザーに一言で確認する。
63
+ 2. **repo 側の準備**:足りない program / ai は `nodes/program/<dir>/` / `nodes/ai/<dir>/` に作る
64
+ (config.json → `npx mawaru typegen` → main.ts 実装 → `npx mawaru validate`)。
65
+ **作った・変えたものは GitHub へ push してから次へ進む**(mawaru は repo を GitHub 経由で
66
+ 読むため、未 push だと graph 保存の検証・書き戻しと食い違って 409 になる)。
67
+ 3. **ループ作成**:`POST loops`。
68
+ 4. **graph ボディを組む**:
69
+ - node / port の `id` はクライアント採番(`uuidgen` などで UUID を作る)
70
+ - program / ai ノードの ports は repo の config.json の inputs / outputs をそのまま写す
71
+ (in ポート key は `in`、out ポートは outputs の各 key)
72
+ - kind 別の固定ポート構成(human / wait / ai)は `port-spec.ts` の `kindPortsViolation` に従う
73
+ - ループ変数(refs=run 全体で読める key→値 の袋)が要るならトップレベルの `refs` に入れる。
74
+ ノード単位の宣言やバインドは無い。handler は実行時に `refs.json` で全量を受け取り、
75
+ `end` の `refs` パッチで値を更新できる(config.json の `refs` はループ定義からの転写=生成物)
76
+ - `position_x` / `position_y` は左→右に流れる単純配置(例: x を 320 刻み、y は 240)
77
+ 5. **保存**:`PUT loops/{loopId}/graph`。400 / 422 / 409 ならメッセージを読んで直して再送。
78
+ 6. **報告**:エディタ URL `{MAWARU_APP_URL}/loops/{loopId}/edit` をユーザーに渡し、
79
+ 見た目の確認と run はユーザーに委ねる。
80
+
81
+ ## 注意
82
+
83
+ - **graph PUT は全量置換**。既存ループを更新するときは、必ず先に `GET loops/{loopId}` で
84
+ 現状を取り、何を変えるかの差分をユーザーに提示して確認してから PUT する。
85
+ - ノードの `kind` は変更不可(変えたいときは別 id で置き直す)。run 履歴から参照されている
86
+ ノードは削除できない(409)。
87
+ - 秘密情報(API キー)をコミットしない。`.env` は .gitignore されていることを確認する。