@mawaru/sdk 0.9.0 → 0.13.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.
@@ -302,6 +302,24 @@ type PropagationNode = {
302
302
  }
303
303
  type PortLink = { from_port_id: string; to_port_id: string }
304
304
 
305
+ // 「上流の out をそのままコピーする」(in)/「in と同型」(approve)を表す指示。
306
+ // 承認ビューのカタログ(@mawaru/common)の COPY_UPSTREAM と同じ値
307
+ export const COPY_UPSTREAM_SCHEMA = "T"
308
+
309
+ // ポートの型宣言:具体型か、上流からのコピー指示か
310
+ export type DeclaredPortSchema = JsonSchema | typeof COPY_UPSTREAM_SCHEMA
311
+
312
+ // 伝播のオプション。sdk は承認ビューのカタログを知らない(非公開。企業向けビューの
313
+ // キーが npm 公開物に載るのを避ける)ので、呼び出し元が解決して渡す
314
+ // → docs/tasks/wip/Humanの入出力を承認ビューの宣言に一本化する.md
315
+ export type PropagationOptions<N> = {
316
+ // human の in / approve に転写する型(承認ビューの宣言)。null / undefined なら
317
+ // その node は対象外(human 以外)。上流ノードの kind は一切見ない
318
+ viewPortSchemasOf?: (
319
+ node: N,
320
+ ) => { in: DeclaredPortSchema; out: DeclaredPortSchema } | null | undefined
321
+ }
322
+
305
323
  // end も導出ノード(各 in は上流から、out main は未宣言)。個別処理は
306
324
  // resolvePortSchemas 内で行うが、初期スキーマを未宣言にするためここに含める。
307
325
  // start も導出ノード:out main は接続先の in から逆向きに転写し、in は out と同型
@@ -342,6 +360,7 @@ const constSchemaOf = (
342
360
  export const resolvePortSchemas = <N extends PropagationNode>(
343
361
  nodes: N[],
344
362
  connections: PortLink[],
363
+ options?: PropagationOptions<N>,
345
364
  ): Map<string, JsonSchema> => {
346
365
  const resolved = new Map<string, JsonSchema>()
347
366
  const kindByPortId = new Map<string, string>()
@@ -427,20 +446,24 @@ export const resolvePortSchemas = <N extends PropagationNode>(
427
446
  if (!isUndeclaredSchema(inSchema)) assign(inPort.id, inSchema)
428
447
  continue
429
448
  }
430
- assign(inPort.id, inSchema)
431
- // human approve:接続元が AI ノードなら封筒 { data, description } を剥がして
432
- // data 型を流す(承認は data だけを下流へ渡す)。それ以外の上流は従来どおり素通し
433
- const fromAiNode =
434
- sources.length === 1 &&
435
- sources[0] !== undefined &&
436
- kindByPortId.get(sources[0]) === "ai"
449
+ // 承認ビューの宣言を human のポートへそのまま転写する(上流ノードの kind は見ない)。
450
+ // "T" は「上流の out をコピー」(in)/「in と同型」(approve)。
451
+ // 上流の out は書き換えない:逆伝播すると fan-out 先のビューが食い違ったとき
452
+ // 型割れが config.json へ write-through され、元の出力宣言を黙って壊す。
453
+ // ズレは接続の型エラーで見せてユーザーの [修正する] で直す
454
+ const declared = options?.viewPortSchemasOf?.(node)
455
+ const effectiveIn =
456
+ declared && declared.in !== COPY_UPSTREAM_SCHEMA
457
+ ? declared.in
458
+ : inSchema
459
+ assign(inPort.id, effectiveIn)
437
460
  for (const port of node.ports) {
438
461
  if (port.io === "out" && isDerivedPort(node.kind, port)) {
439
462
  assign(
440
463
  port.id,
441
- node.kind === "human" && port.key === "approve" && fromAiNode
442
- ? (envelopeDataSchemaOf(inSchema) ?? UNDECLARED_SCHEMA)
443
- : inSchema,
464
+ declared && declared.out !== COPY_UPSTREAM_SCHEMA
465
+ ? declared.out
466
+ : effectiveIn,
444
467
  )
445
468
  }
446
469
  }
@@ -455,8 +478,9 @@ export const resolvePortSchemas = <N extends PropagationNode>(
455
478
  export const propagateDerivedPorts = <N extends PropagationNode>(
456
479
  nodes: N[],
457
480
  connections: PortLink[],
481
+ options?: PropagationOptions<N>,
458
482
  ): N[] => {
459
- const resolved = resolvePortSchemas(nodes, connections)
483
+ const resolved = resolvePortSchemas(nodes, connections, options)
460
484
  return nodes.map((node) => {
461
485
  let touched = false
462
486
  const ports = node.ports.map((port) => {
@@ -0,0 +1,14 @@
1
+ export type Credentials = {
2
+ refresh_token: string;
3
+ access_token: string;
4
+ supabase_url: string;
5
+ supabase_key: string;
6
+ api_url: string;
7
+ email: string;
8
+ };
9
+ export declare const credentialsPath: () => string;
10
+ export declare const loadCredentials: () => Credentials | null;
11
+ export declare const saveCredentials: (credentials: Credentials) => string;
12
+ export declare const clearCredentials: () => boolean;
13
+ export declare const accessTokenFrom: (credentials: Credentials) => Promise<string>;
14
+ export declare const requireCredentials: () => Credentials;
@@ -0,0 +1,81 @@
1
+ import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ export const credentialsPath = () => join(process.env.MAWARU_CONFIG_DIR ?? join(homedir(), ".mawaru"), "credentials.json");
5
+ export const loadCredentials = () => {
6
+ try {
7
+ return JSON.parse(readFileSync(credentialsPath(), "utf8"));
8
+ }
9
+ catch {
10
+ return null;
11
+ }
12
+ };
13
+ // 0600 で保存する(refresh token は実質アカウントそのもの)
14
+ export const saveCredentials = (credentials) => {
15
+ const path = credentialsPath();
16
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
17
+ writeFileSync(path, `${JSON.stringify(credentials, null, 2)}\n`, {
18
+ mode: 0o600,
19
+ });
20
+ chmodSync(path, 0o600);
21
+ return path;
22
+ };
23
+ export const clearCredentials = () => {
24
+ try {
25
+ rmSync(credentialsPath());
26
+ return true;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ };
32
+ // JWT の exp を見て、期限が近ければ refresh する。
33
+ // 検証はしない(署名を確かめるのはサーバの仕事)。ここでは「無駄な 401 を避ける」だけ
34
+ const expiresSoon = (accessToken) => {
35
+ try {
36
+ const payload = accessToken.split(".")[1];
37
+ if (!payload)
38
+ return true;
39
+ const { exp } = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
40
+ if (typeof exp !== "number")
41
+ return true;
42
+ return exp * 1000 - Date.now() < 60_000;
43
+ }
44
+ catch {
45
+ return true;
46
+ }
47
+ };
48
+ // API を叩くための access token。必要なら refresh して credentials を更新する
49
+ export const accessTokenFrom = async (credentials) => {
50
+ if (!expiresSoon(credentials.access_token))
51
+ return credentials.access_token;
52
+ const res = await fetch(`${credentials.supabase_url}/auth/v1/token?grant_type=refresh_token`, {
53
+ method: "POST",
54
+ headers: {
55
+ "content-type": "application/json",
56
+ apikey: credentials.supabase_key,
57
+ },
58
+ body: JSON.stringify({ refresh_token: credentials.refresh_token }),
59
+ });
60
+ if (!res.ok) {
61
+ throw new Error(`セッションの更新に失敗しました(HTTP ${res.status})。\`mawaru login\` でログインし直してください。`);
62
+ }
63
+ const body = (await res.json());
64
+ if (!body.access_token || !body.refresh_token) {
65
+ throw new Error("セッションの更新に失敗しました。`mawaru login` でログインし直してください。");
66
+ }
67
+ saveCredentials({
68
+ ...credentials,
69
+ access_token: body.access_token,
70
+ refresh_token: body.refresh_token,
71
+ });
72
+ return body.access_token;
73
+ };
74
+ // ログイン必須のコマンドの入口。未ログインは案内して終了させる
75
+ export const requireCredentials = () => {
76
+ const credentials = loadCredentials();
77
+ if (!credentials) {
78
+ throw new Error("ログインしていません。`mawaru login` を実行してください。");
79
+ }
80
+ return credentials;
81
+ };
@@ -0,0 +1,5 @@
1
+ export type LoginResult = {
2
+ email: string;
3
+ path: string;
4
+ };
5
+ export declare const runLogin: (timeoutMs?: number) => Promise<LoginResult>;
@@ -0,0 +1,82 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { createServer } from "node:http";
4
+ import { saveCredentials } from "./credentials.js";
5
+ // `gh auth login` と同じループバック方式(RFC 8252)。
6
+ // CLI が 127.0.0.1 の一時ポートで待ち受け、ブラウザで mawaru の /cli-auth を開く。
7
+ // 承認するとブラウザがループバック URL へ**遷移**してトークンを渡す
8
+ //(https のページから http へ fetch は混在コンテンツで塞がれるが、遷移なら通る)。
9
+ const APP_URL = process.env.MAWARU_APP_URL ?? "https://app.mawaru.ai";
10
+ const page = (title, message) => `<!doctype html><meta charset="utf-8"><title>${title}</title>` +
11
+ `<body style="font-family:system-ui;display:grid;place-items:center;height:100vh;margin:0">` +
12
+ `<div style="text-align:center"><h1 style="font-size:1.25rem">${title}</h1>` +
13
+ `<p style="color:#666">${message}</p></div></body>`;
14
+ // ブラウザを開く(開けなければ URL を出して手で開いてもらう。依存は足さない)
15
+ const openBrowser = (url) => {
16
+ const [command, args] = process.platform === "darwin"
17
+ ? ["open", [url]]
18
+ : process.platform === "win32"
19
+ ? ["cmd", ["/c", "start", "", url]]
20
+ : ["xdg-open", [url]];
21
+ try {
22
+ spawn(command, args, { stdio: "ignore", detached: true }).unref();
23
+ }
24
+ catch {
25
+ // 何もしない(URL は既に印字済み)
26
+ }
27
+ };
28
+ export const runLogin = async (timeoutMs = 5 * 60 * 1000) => {
29
+ // 別のローカルプロセスが投げ込んだ応答を受け付けないための照合値
30
+ const state = randomUUID();
31
+ return await new Promise((resolve, reject) => {
32
+ const server = createServer((req, res) => {
33
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
34
+ if (url.pathname !== "/callback") {
35
+ res.writeHead(404).end();
36
+ return;
37
+ }
38
+ const params = url.searchParams;
39
+ const finish = (status, title, message) => {
40
+ res.writeHead(status, { "content-type": "text/html; charset=utf-8" });
41
+ res.end(page(title, message));
42
+ server.close();
43
+ };
44
+ if (params.get("state") !== state) {
45
+ finish(400, "承認に失敗しました", "リクエストが一致しませんでした。");
46
+ reject(new Error("承認の照合値(state)が一致しませんでした"));
47
+ return;
48
+ }
49
+ const credentials = {
50
+ refresh_token: params.get("refresh_token") ?? "",
51
+ access_token: params.get("access_token") ?? "",
52
+ supabase_url: params.get("supabase_url") ?? "",
53
+ supabase_key: params.get("supabase_key") ?? "",
54
+ api_url: params.get("api_url") ?? "",
55
+ email: params.get("email") ?? "",
56
+ };
57
+ if (!credentials.refresh_token || !credentials.api_url) {
58
+ finish(400, "承認に失敗しました", "トークンを受け取れませんでした。");
59
+ reject(new Error("承認の応答にトークンが含まれていませんでした"));
60
+ return;
61
+ }
62
+ const path = saveCredentials(credentials);
63
+ finish(200, "ログインしました", "このタブを閉じて、ターミナルに戻ってください。");
64
+ resolve({ email: credentials.email, path });
65
+ });
66
+ server.on("error", reject);
67
+ // ループバックのみに束ねる(他ホストから叩かれないように)
68
+ server.listen(0, "127.0.0.1", () => {
69
+ const { port } = server.address();
70
+ const authUrl = new URL("/cli-auth", APP_URL);
71
+ authUrl.searchParams.set("callback", `http://127.0.0.1:${port}/callback`);
72
+ authUrl.searchParams.set("state", state);
73
+ console.log(`ブラウザで承認してください: ${authUrl}`);
74
+ openBrowser(authUrl.toString());
75
+ });
76
+ const timer = setTimeout(() => {
77
+ server.close();
78
+ reject(new Error("承認がタイムアウトしました。もう一度お試しください。"));
79
+ }, timeoutMs);
80
+ server.on("close", () => clearTimeout(timer));
81
+ });
82
+ };
@@ -0,0 +1,21 @@
1
+ import { type Credentials } from "./credentials.js";
2
+ export type StepRef = {
3
+ tenantId: string;
4
+ runId: string;
5
+ stepId: string;
6
+ };
7
+ export declare const parseStepUrl: (raw: string) => StepRef;
8
+ export declare const recordedCwds: (transcript: string) => {
9
+ all: string[];
10
+ last: string | null;
11
+ };
12
+ export declare const findProjectDir: (cwd: string) => string | null;
13
+ export type TranscriptRef = {
14
+ url: string;
15
+ session_id: string;
16
+ redacted?: number;
17
+ };
18
+ export declare const fetchTranscriptRef: (credentials: Credentials, ref: StepRef) => Promise<TranscriptRef>;
19
+ export declare const downloadTranscript: (url: string) => Promise<string>;
20
+ export declare const placeTranscript: (cwd: string, sessionId: string, transcript: string) => string;
21
+ export declare const spawnClaudeResume: (sessionId: string) => Promise<number>;
@@ -0,0 +1,125 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { accessTokenFrom } from "./credentials.js";
6
+ // UI がコピーさせる web の URL から tenant / run / step を取り出す。
7
+ // 3つを1つの貼れる文字列で運ぶために URL の形にしている
8
+ export const parseStepUrl = (raw) => {
9
+ let url;
10
+ try {
11
+ url = new URL(raw);
12
+ }
13
+ catch {
14
+ throw new Error(`URL として読めません: ${raw}`);
15
+ }
16
+ const segments = url.pathname.split("/").filter(Boolean);
17
+ const runIndex = segments.indexOf("runs");
18
+ const stepIndex = segments.indexOf("steps");
19
+ const runId = runIndex >= 0 ? segments[runIndex + 1] : undefined;
20
+ const stepId = stepIndex >= 0 ? segments[stepIndex + 1] : undefined;
21
+ const tenantId = url.searchParams.get("tenant");
22
+ if (!runId || !stepId) {
23
+ throw new Error("run / step を含む URL ではありません(例: https://app.mawaru.ai/runs/<runId>/steps/<stepId>?tenant=<tenantId>)");
24
+ }
25
+ if (!tenantId) {
26
+ throw new Error("URL に ?tenant=<tenantId> がありません。run 詳細のコピーボタンで取得した URL をそのまま渡してください。");
27
+ }
28
+ return { tenantId, runId, stepId };
29
+ };
30
+ // transcript に記録された作業ディレクトリ(重複は畳み、最後に居た場所も返す)。
31
+ // **現在地がそこと違っていても resume 自体は通ってしまう**ので、人間に見せて確認を取る
32
+ export const recordedCwds = (transcript) => {
33
+ const all = [];
34
+ let last = null;
35
+ for (const line of transcript.split("\n")) {
36
+ if (!line)
37
+ continue;
38
+ let cwd;
39
+ try {
40
+ cwd = JSON.parse(line).cwd;
41
+ }
42
+ catch {
43
+ continue;
44
+ }
45
+ if (typeof cwd !== "string" || cwd.length === 0)
46
+ continue;
47
+ if (!all.includes(cwd))
48
+ all.push(cwd);
49
+ last = cwd;
50
+ }
51
+ return { all, last };
52
+ };
53
+ const projectsRoot = () => join(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude"), "projects");
54
+ // 置き先のディレクトリを **規則を再実装せず観測で決める**(agent-runner の
55
+ // _helpers/session.ts と同じ方針)。~/.claude/projects/*/*.jsonl の cwd を読み、
56
+ // いま居るディレクトリと一致するものを選ぶ
57
+ export const findProjectDir = (cwd) => {
58
+ const root = projectsRoot();
59
+ if (!existsSync(root))
60
+ return null;
61
+ for (const dir of readdirSync(root)) {
62
+ const path = join(root, dir);
63
+ let files;
64
+ try {
65
+ files = readdirSync(path).filter((f) => f.endsWith(".jsonl"));
66
+ }
67
+ catch {
68
+ continue;
69
+ }
70
+ for (const file of files) {
71
+ let head;
72
+ try {
73
+ head = readFileSync(join(path, file), "utf8");
74
+ }
75
+ catch {
76
+ continue;
77
+ }
78
+ if (recordedCwds(head).all.includes(cwd))
79
+ return path;
80
+ }
81
+ }
82
+ return null;
83
+ };
84
+ // 段階3 のエンドポイントから署名 URL と session_id を得る
85
+ export const fetchTranscriptRef = async (credentials, ref) => {
86
+ const token = await accessTokenFrom(credentials);
87
+ const res = await fetch(`${credentials.api_url}/tenants/${ref.tenantId}/runs/${ref.runId}/steps/${ref.stepId}/transcript`, { headers: { Authorization: `Bearer ${token}` } });
88
+ if (res.status === 404) {
89
+ throw new Error("この step には退避されたセッションがありません(AI ノード以外か、退避前の実行です)。");
90
+ }
91
+ if (!res.ok) {
92
+ throw new Error(`transcript を取得できませんでした(HTTP ${res.status})`);
93
+ }
94
+ return (await res.json());
95
+ };
96
+ export const downloadTranscript = async (url) => {
97
+ const res = await fetch(url);
98
+ if (!res.ok) {
99
+ throw new Error(`transcript のダウンロードに失敗しました(HTTP ${res.status})`);
100
+ }
101
+ return await res.text();
102
+ };
103
+ // 置き先へ書き込む。ディレクトリが見つからないのは「この repo でまだ claude を
104
+ // 起動していない」ケースなので、観測できるようにしてから再実行してもらう
105
+ export const placeTranscript = (cwd, sessionId, transcript) => {
106
+ const dir = findProjectDir(cwd);
107
+ if (!dir) {
108
+ throw new Error("このディレクトリの Claude Code プロジェクトが見つかりません。" +
109
+ "ここで一度 `claude` を起動してから、もう一度実行してください。");
110
+ }
111
+ mkdirSync(dir, { recursive: true });
112
+ const file = join(dir, `${sessionId}.jsonl`);
113
+ writeFileSync(file, transcript);
114
+ return file;
115
+ };
116
+ // stdio を引き継いで claude を起動する(そのまま対話に入る)
117
+ export const spawnClaudeResume = (sessionId) => new Promise((resolve) => {
118
+ const child = spawn("claude", ["--resume", sessionId], { stdio: "inherit" });
119
+ child.on("error", () => {
120
+ console.error("claude を起動できませんでした。Claude Code をインストールしてから `claude --resume " +
121
+ `${sessionId}\` を実行してください。`);
122
+ resolve(1);
123
+ });
124
+ child.on("close", (code) => resolve(code ?? 0));
125
+ });
@@ -0,0 +1,3 @@
1
+ export declare const runSessionResume: (stepUrl: string | undefined, options: {
2
+ yes: boolean;
3
+ }) => Promise<number>;
@@ -0,0 +1,55 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { requireCredentials } from "./credentials.js";
3
+ import { downloadTranscript, fetchTranscriptRef, parseStepUrl, placeTranscript, recordedCwds, spawnClaudeResume, } from "./resumeSession.js";
4
+ const confirm = async (question) => {
5
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
6
+ try {
7
+ const answer = await rl.question(`${question} [y/N] `);
8
+ return /^y(es)?$/i.test(answer.trim());
9
+ }
10
+ finally {
11
+ rl.close();
12
+ }
13
+ };
14
+ // `mawaru session resume <step URL> [--yes]`
15
+ //
16
+ // 取得 → 作業ディレクトリの確認 → 配置 → `claude --resume`。
17
+ // **どこに cd すべきかは mawaru には分からない**(AI ノードの実行 repo と、エージェントが
18
+ // 実際に触った repo は別物で、後者は setup.sh が workspace/ 配下に clone した対象 repo)。
19
+ // なので推定せず、記録された cwd をそのまま見せて人間に判断させる
20
+ export const runSessionResume = async (stepUrl, options) => {
21
+ if (!stepUrl) {
22
+ console.error("使い方: mawaru session resume <step URL> [--yes]\n" +
23
+ "run 詳細の AI ノードにあるコピーボタンで URL を取得できます。");
24
+ return 1;
25
+ }
26
+ // URL の誤りは通信より先に落とす
27
+ const ref = parseStepUrl(stepUrl);
28
+ const credentials = requireCredentials();
29
+ const transcriptRef = await fetchTranscriptRef(credentials, ref);
30
+ const transcript = await downloadTranscript(transcriptRef.url);
31
+ console.log(`取得: セッション ${transcriptRef.session_id}`);
32
+ if (transcriptRef.redacted) {
33
+ console.log(`(退避時に secret を ${transcriptRef.redacted} 箇所マスクしています)`);
34
+ }
35
+ const { all, last } = recordedCwds(transcript);
36
+ const cwd = process.cwd();
37
+ if (all.length > 0) {
38
+ console.log("このセッションの作業ディレクトリ:");
39
+ for (const dir of all) {
40
+ console.log(` ${dir}${dir === last ? "(最後に居た場所)" : ""}`);
41
+ }
42
+ }
43
+ console.log(`現在地:\n ${cwd}`);
44
+ if (!all.includes(cwd)) {
45
+ console.log("※ 記録された作業ディレクトリとは違います。記録上の絶対パスはここでは解決しません" +
46
+ "(エージェントは読めないパスに当たれば自分で見つけ直します)。");
47
+ }
48
+ if (!options.yes && !(await confirm("続けますか?"))) {
49
+ console.log("中止しました。");
50
+ return 1;
51
+ }
52
+ const file = placeTranscript(cwd, transcriptRef.session_id, transcript);
53
+ console.log(`配置: ${file}\n再開します…`);
54
+ return await spawnClaudeResume(transcriptRef.session_id);
55
+ };
@@ -1,17 +1,19 @@
1
1
  import { z } from "zod";
2
- export declare const AI_MODELS: readonly ["claude-opus-4-8", "claude-sonnet-5", "claude-haiku-4-5"];
2
+ export declare const AI_MODELS: readonly ["claude-opus-5", "claude-opus-4-8", "claude-sonnet-5", "claude-haiku-4-5"];
3
3
  export declare const aiModelSchema: z.ZodEnum<{
4
+ "claude-opus-5": "claude-opus-5";
4
5
  "claude-opus-4-8": "claude-opus-4-8";
5
6
  "claude-sonnet-5": "claude-sonnet-5";
6
7
  "claude-haiku-4-5": "claude-haiku-4-5";
7
8
  }>;
8
9
  export type AiModel = z.infer<typeof aiModelSchema>;
9
- export declare const DEFAULT_AI_MODEL = "claude-opus-4-8";
10
+ export declare const DEFAULT_AI_MODEL = "claude-opus-5";
10
11
  export declare const aiManifestSchema: z.ZodObject<{
11
12
  name: z.ZodOptional<z.ZodString>;
12
13
  description: z.ZodOptional<z.ZodString>;
13
14
  prompt: z.ZodString;
14
15
  model: z.ZodDefault<z.ZodEnum<{
16
+ "claude-opus-5": "claude-opus-5";
15
17
  "claude-opus-4-8": "claude-opus-4-8";
16
18
  "claude-sonnet-5": "claude-sonnet-5";
17
19
  "claude-haiku-4-5": "claude-haiku-4-5";
@@ -1,14 +1,17 @@
1
1
  import { z } from "zod";
2
2
  import { jsonSchemaSchema } from "./node.js";
3
3
  import { isAiEnvelopeSchema, manifestInKeysViolation } from "./port-spec.js";
4
- // AI ノードのモデル選択肢(既定は Opus:下書き品質が価値の中心。費用はユーザー持ち)
4
+ // AI ノードのモデル選択肢(既定は Opus:下書き品質が価値の中心。費用はユーザー持ち)。
5
+ // エイリアス("opus" 等)は採らずバージョンを固定する:実行のたびに黙って別モデルへ
6
+ // 乗り換わると品質も費用も再現しないため。新モデルが出たらこの配列を足して SDK を publish する
5
7
  export const AI_MODELS = [
8
+ "claude-opus-5",
6
9
  "claude-opus-4-8",
7
10
  "claude-sonnet-5",
8
11
  "claude-haiku-4-5",
9
12
  ];
10
13
  export const aiModelSchema = z.enum(AI_MODELS);
11
- export const DEFAULT_AI_MODEL = "claude-opus-4-8";
14
+ export const DEFAULT_AI_MODEL = "claude-opus-5";
12
15
  // nodes/ai/<dir>/config.json(実行repoの構造見直し)。AI ノードの定義(prompt/model/skills/
13
16
  // 入出力スキーマ/env)は repo 側が正で、エディタが取り込んで ports を自動生成する
14
17
  // (実行時の正は ports テーブルのまま。プログラム規約 v2 と同じ関係)。
@@ -0,0 +1,12 @@
1
+ import type { GraphConnection, GraphNode } from "./graph.js";
2
+ export declare const duplicateSelection: (params: {
3
+ nodes: GraphNode[];
4
+ connections: GraphConnection[];
5
+ selectedIds: readonly string[];
6
+ dx: number;
7
+ dy: number;
8
+ newId: () => string;
9
+ }) => {
10
+ nodes: GraphNode[];
11
+ connections: GraphConnection[];
12
+ };
@@ -0,0 +1,34 @@
1
+ // 選択ノードのコピー&ペースト・複製(ノードのコピー&ペーストと複数選択.md)。
2
+ // 純関数(id 採番は newId で注入)。エディタとテストの両方から使う。
3
+ //
4
+ // グラフは全量置換 PUT なので、複製は「id を全部採番し直して足す」だけで成立する。
5
+ // ポート数の制約(in は kind ごとに固定・out は1以上)や (io,key) のノード内一意は
6
+ // ノードを丸ごと写すので自動的に保たれる。
7
+ export const duplicateSelection = (params) => {
8
+ const { nodes, connections, selectedIds, dx, dy, newId } = params;
9
+ const selected = new Set(selectedIds);
10
+ // start / end は回路の構造ノードなので複製しない(選択に混ざっていても落とす)。
11
+ // 並び順は元グラフのまま(選択の指定順に依存させない)
12
+ const portIdMap = new Map();
13
+ const copied = nodes
14
+ .filter((n) => selected.has(n.id) && n.kind !== "start" && n.kind !== "end")
15
+ .map((n) => ({
16
+ ...n,
17
+ id: newId(),
18
+ ports: n.ports.map((p) => {
19
+ const portId = newId();
20
+ portIdMap.set(p.id, portId);
21
+ return { ...p, id: portId };
22
+ }),
23
+ position_x: n.position_x + dx,
24
+ position_y: n.position_y + dy,
25
+ }));
26
+ // 両端が複製対象のポートである線(=選択内で完結する線)だけを張り替えて写す。
27
+ // 外部との線(入口・出口)はコピーしない
28
+ const copiedConnections = connections.flatMap((c) => {
29
+ const from = portIdMap.get(c.from_port_id);
30
+ const to = portIdMap.get(c.to_port_id);
31
+ return from && to ? [{ from_port_id: from, to_port_id: to }] : [];
32
+ });
33
+ return { nodes: copied, connections: copiedConnections };
34
+ };
@@ -17,12 +17,12 @@ export declare const editableNodeKindSchema: z.ZodEnum<{
17
17
  }>;
18
18
  export type EditableNodeKind = z.infer<typeof editableNodeKindSchema>;
19
19
  export declare const graphNodeKindSchema: z.ZodEnum<{
20
+ start: "start";
21
+ end: "end";
20
22
  ai: "ai";
21
23
  human: "human";
22
24
  program: "program";
23
25
  wait: "wait";
24
- start: "start";
25
- end: "end";
26
26
  component: "component";
27
27
  }>;
28
28
  export type GraphNodeKind = z.infer<typeof graphNodeKindSchema>;
@@ -52,15 +52,14 @@ export declare const assignMemberSchema: z.ZodUnion<readonly [z.ZodUUID, z.ZodLi
52
52
  export type AssignMember = z.infer<typeof assignMemberSchema>;
53
53
  export declare const humanConfigSchema: z.ZodObject<{
54
54
  view: z.ZodOptional<z.ZodString>;
55
- view_config: z.ZodOptional<z.ZodNullable<z.ZodUnknown>>;
56
55
  assignment: z.ZodObject<{
57
56
  strategy: z.ZodDefault<z.ZodEnum<{
58
57
  fixed: "fixed";
59
58
  round_robin: "round_robin";
60
59
  }>>;
61
60
  approval_mode: z.ZodDefault<z.ZodEnum<{
62
- any: "any";
63
61
  all: "all";
62
+ any: "any";
64
63
  }>>;
65
64
  units: z.ZodDefault<z.ZodArray<z.ZodArray<z.ZodUnion<readonly [z.ZodUUID, z.ZodLiteral<"run_starter">]>>>>;
66
65
  }, z.core.$strip>;
@@ -81,12 +80,12 @@ export type GraphNodeHook = z.infer<typeof graphNodeHookSchema>;
81
80
  export declare const graphNodeSchema: z.ZodObject<{
82
81
  id: z.ZodUUID;
83
82
  kind: z.ZodEnum<{
83
+ start: "start";
84
+ end: "end";
84
85
  ai: "ai";
85
86
  human: "human";
86
87
  program: "program";
87
88
  wait: "wait";
88
- start: "start";
89
- end: "end";
90
89
  component: "component";
91
90
  }>;
92
91
  name: z.ZodString;
@@ -136,15 +135,14 @@ export declare const graphNodeSchema: z.ZodObject<{
136
135
  }, z.core.$strip>>;
137
136
  human: z.ZodOptional<z.ZodObject<{
138
137
  view: z.ZodOptional<z.ZodString>;
139
- view_config: z.ZodOptional<z.ZodNullable<z.ZodUnknown>>;
140
138
  assignment: z.ZodObject<{
141
139
  strategy: z.ZodDefault<z.ZodEnum<{
142
140
  fixed: "fixed";
143
141
  round_robin: "round_robin";
144
142
  }>>;
145
143
  approval_mode: z.ZodDefault<z.ZodEnum<{
146
- any: "any";
147
144
  all: "all";
145
+ any: "any";
148
146
  }>>;
149
147
  units: z.ZodDefault<z.ZodArray<z.ZodArray<z.ZodUnion<readonly [z.ZodUUID, z.ZodLiteral<"run_starter">]>>>>;
150
148
  }, z.core.$strip>;
@@ -163,12 +161,12 @@ export declare const saveGraphBodySchema: z.ZodObject<{
163
161
  nodes: z.ZodArray<z.ZodObject<{
164
162
  id: z.ZodUUID;
165
163
  kind: z.ZodEnum<{
164
+ start: "start";
165
+ end: "end";
166
166
  ai: "ai";
167
167
  human: "human";
168
168
  program: "program";
169
169
  wait: "wait";
170
- start: "start";
171
- end: "end";
172
170
  component: "component";
173
171
  }>;
174
172
  name: z.ZodString;
@@ -218,15 +216,14 @@ export declare const saveGraphBodySchema: z.ZodObject<{
218
216
  }, z.core.$strip>>;
219
217
  human: z.ZodOptional<z.ZodObject<{
220
218
  view: z.ZodOptional<z.ZodString>;
221
- view_config: z.ZodOptional<z.ZodNullable<z.ZodUnknown>>;
222
219
  assignment: z.ZodObject<{
223
220
  strategy: z.ZodDefault<z.ZodEnum<{
224
221
  fixed: "fixed";
225
222
  round_robin: "round_robin";
226
223
  }>>;
227
224
  approval_mode: z.ZodDefault<z.ZodEnum<{
228
- any: "any";
229
225
  all: "all";
226
+ any: "any";
230
227
  }>>;
231
228
  units: z.ZodDefault<z.ZodArray<z.ZodArray<z.ZodUnion<readonly [z.ZodUUID, z.ZodLiteral<"run_starter">]>>>>;
232
229
  }, z.core.$strip>;