ima2-gen 1.1.11 → 1.1.12

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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/bin/commands/capabilities.js +6 -0
  3. package/bin/commands/capabilities.ts +6 -0
  4. package/bin/commands/config.js +33 -2
  5. package/bin/commands/config.ts +36 -1
  6. package/bin/commands/prompt-sub/build.js +102 -0
  7. package/bin/commands/prompt-sub/build.ts +101 -0
  8. package/bin/commands/prompt.js +74 -1
  9. package/bin/commands/prompt.ts +70 -1
  10. package/bin/ima2.js +49 -19
  11. package/bin/ima2.ts +50 -17
  12. package/bin/lib/config-store.js +5 -51
  13. package/bin/lib/config-store.ts +11 -54
  14. package/bin/lib/destructive-confirm.js +16 -0
  15. package/bin/lib/destructive-confirm.ts +19 -0
  16. package/bin/lib/doctor-checks.js +75 -0
  17. package/bin/lib/doctor-checks.ts +91 -0
  18. package/bin/lib/ui-build.js +75 -0
  19. package/bin/lib/ui-build.ts +85 -0
  20. package/docs/CLI.md +7 -1
  21. package/docs/migration/runtime-test-inventory.md +22 -1
  22. package/lib/agentCommandParser.js +64 -0
  23. package/lib/agentCommandParser.ts +69 -0
  24. package/lib/agentGenerationPlanner.js +185 -0
  25. package/lib/agentGenerationPlanner.ts +229 -0
  26. package/lib/agentQuestionResponder.js +207 -0
  27. package/lib/agentQuestionResponder.ts +266 -0
  28. package/lib/agentQueueStore.js +217 -0
  29. package/lib/agentQueueStore.ts +270 -0
  30. package/lib/agentQueueWorker.js +83 -0
  31. package/lib/agentQueueWorker.ts +89 -0
  32. package/lib/agentRuntime.js +300 -0
  33. package/lib/agentRuntime.ts +404 -0
  34. package/lib/agentSettings.js +60 -0
  35. package/lib/agentSettings.ts +72 -0
  36. package/lib/agentStore.js +310 -0
  37. package/lib/agentStore.ts +422 -0
  38. package/lib/agentStoreRows.js +80 -0
  39. package/lib/agentStoreRows.ts +136 -0
  40. package/lib/agentTypes.js +5 -0
  41. package/lib/agentTypes.ts +153 -0
  42. package/lib/apiCachePolicy.js +8 -0
  43. package/lib/apiCachePolicy.ts +11 -0
  44. package/lib/capabilities.js +25 -0
  45. package/lib/capabilities.ts +25 -0
  46. package/lib/composerSnapshot.js +23 -0
  47. package/lib/composerSnapshot.ts +33 -0
  48. package/lib/configKeys.js +52 -0
  49. package/lib/configKeys.ts +57 -0
  50. package/lib/db.js +125 -4
  51. package/lib/db.ts +138 -9
  52. package/lib/historyList.js +4 -0
  53. package/lib/historyList.ts +4 -0
  54. package/lib/promptBuilder/attachments.js +54 -0
  55. package/lib/promptBuilder/attachments.ts +74 -0
  56. package/lib/promptBuilder/client.js +86 -0
  57. package/lib/promptBuilder/client.ts +130 -0
  58. package/lib/promptBuilder/constants.js +9 -0
  59. package/lib/promptBuilder/constants.ts +9 -0
  60. package/lib/promptBuilder/context.js +27 -0
  61. package/lib/promptBuilder/context.ts +36 -0
  62. package/lib/promptBuilder/errors.js +6 -0
  63. package/lib/promptBuilder/errors.ts +12 -0
  64. package/lib/promptBuilder/requestSchema.js +40 -0
  65. package/lib/promptBuilder/requestSchema.ts +56 -0
  66. package/lib/promptBuilder/responseParser.js +181 -0
  67. package/lib/promptBuilder/responseParser.ts +219 -0
  68. package/lib/promptBuilder/systemPrompt.js +135 -0
  69. package/lib/promptBuilder/systemPrompt.ts +135 -0
  70. package/lib/promptBuilder/transport.js +67 -0
  71. package/lib/promptBuilder/transport.ts +94 -0
  72. package/lib/promptBuilder/types.js +1 -0
  73. package/lib/promptBuilder/types.ts +109 -0
  74. package/lib/responsesImageAdapter.js +43 -3
  75. package/lib/responsesImageAdapter.ts +61 -5
  76. package/package.json +6 -4
  77. package/routes/agent.js +259 -0
  78. package/routes/agent.ts +308 -0
  79. package/routes/generate.js +7 -0
  80. package/routes/generate.ts +12 -0
  81. package/routes/index.js +4 -0
  82. package/routes/index.ts +4 -0
  83. package/routes/multimode.js +15 -0
  84. package/routes/multimode.ts +20 -0
  85. package/routes/promptBuilder.js +27 -0
  86. package/routes/promptBuilder.ts +37 -0
  87. package/server.js +2 -0
  88. package/server.ts +2 -0
  89. package/skills/ima2/SKILL.md +41 -0
  90. package/ui/dist/.vite/manifest.json +30 -10
  91. package/ui/dist/assets/AgentWorkspace-BJe9yxPA.js +3 -0
  92. package/ui/dist/assets/{CardNewsWorkspace-j4ULtNdk.js → CardNewsWorkspace-BBLdwzYU.js} +1 -1
  93. package/ui/dist/assets/{NodeCanvas-Bc7BUViM.js → NodeCanvas-BSZ527J4.js} +1 -1
  94. package/ui/dist/assets/PromptBuilderPanel-Y2VygFc0.js +2 -0
  95. package/ui/dist/assets/{PromptImportDialog-DBKprBEo.js → PromptImportDialog-C6lFV-LL.js} +2 -2
  96. package/ui/dist/assets/{PromptImportDiscoverySection-m5v55Zsy.js → PromptImportDiscoverySection-D8YJFhND.js} +1 -1
  97. package/ui/dist/assets/{PromptImportFolderSection-DnPvJkfJ.js → PromptImportFolderSection-ywfcQolW.js} +1 -1
  98. package/ui/dist/assets/{PromptLibraryPanel-BMSqfK9C.js → PromptLibraryPanel-fk4KmrGy.js} +2 -2
  99. package/ui/dist/assets/SettingsWorkspace-DL5vhAHQ.js +1 -0
  100. package/ui/dist/assets/index-BLx55BOg.js +1 -0
  101. package/ui/dist/assets/index-ByViUJfx.css +1 -0
  102. package/ui/dist/assets/index-Ci36vcFD.js +28 -0
  103. package/ui/dist/index.html +2 -2
  104. package/ui/dist/assets/SettingsWorkspace-Cj3LD0uu.js +0 -1
  105. package/ui/dist/assets/index-9aOJKFI-.js +0 -1
  106. package/ui/dist/assets/index-De-AWE6B.css +0 -1
  107. package/ui/dist/assets/index-tQhOLR-C.js +0 -28
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jun
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -67,6 +67,12 @@ function printText(capabilities) {
67
67
  out(` models: ${capabilities.valid?.imageModels?.supported?.join(", ")}`);
68
68
  out(` reasoning: ${capabilities.valid?.reasoningEfforts?.join(", ")}`);
69
69
  out(` quality: ${capabilities.valid?.quality?.join(", ")}`);
70
+ out(` modes: ${capabilities.valid?.modes?.join(", ")}`);
71
+ out(` moderation: ${capabilities.valid?.moderation?.join(", ")}`);
72
+ out(` providers: ${capabilities.valid?.providers?.join(", ")}`);
73
+ out("");
74
+ out(`config keys: ${capabilities.configKeys?.writable?.length ?? 0} writable`);
75
+ out(color.dim("run: ima2 config keys --json"));
70
76
  out("");
71
77
  out(`limits: refs=${capabilities.limits?.maxRefCount}, images=${capabilities.limits?.maxGeneratedImages}`);
72
78
  out(color.dim(`maxParallel: ${capabilities.limits?.maxParallel?.value} (${capabilities.limits?.maxParallel?.note})`));
@@ -71,6 +71,12 @@ function printText(capabilities: any): void {
71
71
  out(` models: ${capabilities.valid?.imageModels?.supported?.join(", ")}`);
72
72
  out(` reasoning: ${capabilities.valid?.reasoningEfforts?.join(", ")}`);
73
73
  out(` quality: ${capabilities.valid?.quality?.join(", ")}`);
74
+ out(` modes: ${capabilities.valid?.modes?.join(", ")}`);
75
+ out(` moderation: ${capabilities.valid?.moderation?.join(", ")}`);
76
+ out(` providers: ${capabilities.valid?.providers?.join(", ")}`);
77
+ out("");
78
+ out(`config keys: ${capabilities.configKeys?.writable?.length ?? 0} writable`);
79
+ out(color.dim("run: ima2 config keys --json"));
74
80
  out("");
75
81
  out(`limits: refs=${capabilities.limits?.maxRefCount}, images=${capabilities.limits?.maxGeneratedImages}`);
76
82
  out(color.dim(`maxParallel: ${capabilities.limits?.maxParallel?.value} (${capabilities.limits?.maxParallel?.note})`));
@@ -1,7 +1,8 @@
1
1
  import { createInterface } from "readline/promises";
2
2
  import { parseArgs } from "../lib/args.js";
3
+ import { confirmDestructiveAction } from "../lib/destructive-confirm.js";
3
4
  import { out, die, color, json } from "../lib/output.js";
4
- import { CONFIG_FILE, buildEffectiveConfig, deleteNestedKey, displayPath, envOverrideForKey, getNestedKey, isAuthConfigKey, isSensitiveConfigKey, isWritableConfigKey, loadFileCfg, parseConfigValue, redactValue, restartNotice, saveFileCfg, setNestedKey, } from "../lib/config-store.js";
5
+ import { CONFIG_FILE, KEY_TO_ENV, WRITABLE_CONFIG_KEYS, buildEffectiveConfig, deleteNestedKey, displayPath, envOverrideForKey, getNestedKey, isAuthConfigKey, isSensitiveConfigKey, isWritableConfigKey, loadFileCfg, parseConfigValue, redactValue, restartNotice, saveFileCfg, setNestedKey, } from "../lib/config-store.js";
5
6
  const HELP = `
6
7
  ima2 config <subcommand> [options]
7
8
 
@@ -10,7 +11,8 @@ const HELP = `
10
11
  ls [--effective] [--json] List file layer (or merged effective config with --effective)
11
12
  get <key> [--json] Get a dotted key from effective config (redacts secrets)
12
13
  set <key> <value> [-y] Write a key to the file layer
13
- rm <key> Remove a key from the file layer
14
+ rm <key> [-y] Remove a key from the file layer
15
+ keys [--json] List writable keys and environment overrides
14
16
 
15
17
  Keys use dot notation, e.g.: imageModels.default, log.level, features.cardNews
16
18
 
@@ -66,6 +68,20 @@ async function getSub(argv) {
66
68
  out(typeof value === "object" ? JSON.stringify(value, null, 2) : String(value));
67
69
  }
68
70
  }
71
+ async function keysSub(argv) {
72
+ const args = parseArgs(argv, { flags: FLAGS });
73
+ const keys = [...WRITABLE_CONFIG_KEYS].sort().map((key) => ({
74
+ key,
75
+ env: KEY_TO_ENV[key] ?? null,
76
+ }));
77
+ if (args.json) {
78
+ json({ keys });
79
+ return;
80
+ }
81
+ for (const item of keys) {
82
+ out(item.env ? `${item.key} (${item.env})` : item.key);
83
+ }
84
+ }
69
85
  async function setSub(argv) {
70
86
  const args = parseArgs(argv, { flags: FLAGS });
71
87
  const [key, rawValue] = args.positional;
@@ -109,6 +125,20 @@ async function rmSub(argv) {
109
125
  die(2, `"${key}" is an auth key. Use 'ima2 setup' or 'ima2 login' to change authentication.`);
110
126
  }
111
127
  const fileCfg = loadFileCfg();
128
+ if (getNestedKey(fileCfg, key) === undefined) {
129
+ out(color.dim(`(key not found in file layer: ${key})`));
130
+ return;
131
+ }
132
+ try {
133
+ const confirmed = await confirmDestructiveAction(`Remove config key "${key}"?`, Boolean(args.yes));
134
+ if (!confirmed) {
135
+ out("Aborted.");
136
+ return;
137
+ }
138
+ }
139
+ catch (err) {
140
+ die(2, err instanceof Error ? err.message : String(err));
141
+ }
112
142
  const removed = deleteNestedKey(fileCfg, key);
113
143
  if (!removed) {
114
144
  out(color.dim(`(key not found in file layer: ${key})`));
@@ -124,6 +154,7 @@ const SUB = {
124
154
  get: getSub,
125
155
  set: setSub,
126
156
  rm: rmSub,
157
+ keys: keysSub,
127
158
  };
128
159
  export default async function configCmd(argv) {
129
160
  const sub = argv[0];
@@ -1,8 +1,11 @@
1
1
  import { createInterface } from "readline/promises";
2
2
  import { parseArgs } from "../lib/args.js";
3
+ import { confirmDestructiveAction } from "../lib/destructive-confirm.js";
3
4
  import { out, die, color, json } from "../lib/output.js";
4
5
  import {
5
6
  CONFIG_FILE,
7
+ KEY_TO_ENV,
8
+ WRITABLE_CONFIG_KEYS,
6
9
  buildEffectiveConfig,
7
10
  deleteNestedKey,
8
11
  displayPath,
@@ -27,7 +30,8 @@ const HELP = `
27
30
  ls [--effective] [--json] List file layer (or merged effective config with --effective)
28
31
  get <key> [--json] Get a dotted key from effective config (redacts secrets)
29
32
  set <key> <value> [-y] Write a key to the file layer
30
- rm <key> Remove a key from the file layer
33
+ rm <key> [-y] Remove a key from the file layer
34
+ keys [--json] List writable keys and environment overrides
31
35
 
32
36
  Keys use dot notation, e.g.: imageModels.default, log.level, features.cardNews
33
37
 
@@ -76,6 +80,21 @@ async function getSub(argv: string[]) {
76
80
  }
77
81
  }
78
82
 
83
+ async function keysSub(argv: string[]) {
84
+ const args = parseArgs(argv, { flags: FLAGS });
85
+ const keys = [...WRITABLE_CONFIG_KEYS].sort().map((key) => ({
86
+ key,
87
+ env: KEY_TO_ENV[key] ?? null,
88
+ }));
89
+ if (args.json) {
90
+ json({ keys });
91
+ return;
92
+ }
93
+ for (const item of keys) {
94
+ out(item.env ? `${item.key} (${item.env})` : item.key);
95
+ }
96
+ }
97
+
79
98
  async function setSub(argv: string[]) {
80
99
  const args = parseArgs(argv, { flags: FLAGS });
81
100
  const [key, rawValue] = args.positional;
@@ -123,6 +142,21 @@ async function rmSub(argv: string[]) {
123
142
  }
124
143
 
125
144
  const fileCfg = loadFileCfg();
145
+ if (getNestedKey(fileCfg, key) === undefined) {
146
+ out(color.dim(`(key not found in file layer: ${key})`));
147
+ return;
148
+ }
149
+
150
+ try {
151
+ const confirmed = await confirmDestructiveAction(`Remove config key "${key}"?`, Boolean(args.yes));
152
+ if (!confirmed) {
153
+ out("Aborted.");
154
+ return;
155
+ }
156
+ } catch (err) {
157
+ die(2, err instanceof Error ? err.message : String(err));
158
+ }
159
+
126
160
  const removed = deleteNestedKey(fileCfg, key);
127
161
  if (!removed) {
128
162
  out(color.dim(`(key not found in file layer: ${key})`));
@@ -140,6 +174,7 @@ const SUB: Record<string, Sub> = {
140
174
  get: getSub,
141
175
  set: setSub,
142
176
  rm: rmSub,
177
+ keys: keysSub,
143
178
  };
144
179
 
145
180
  export default async function configCmd(argv: string[]) {
@@ -0,0 +1,102 @@
1
+ import { readFile } from "fs/promises";
2
+ import { parseArgs } from "../../lib/args.js";
3
+ import { resolveServer, request } from "../../lib/client.js";
4
+ import { readStdin } from "../../lib/files.js";
5
+ import { out, die, color, json, exitCodeForError } from "../../lib/output.js";
6
+ const BUILD_HELP = `
7
+ ima2 prompt build [options]
8
+
9
+ Refine prompt intent through the embedded prompt builder.
10
+
11
+ Options:
12
+ --message <text> User message (required unless --messages)
13
+ --messages <file|@file|-> Multi-turn conversation as JSON array
14
+ --ref <image> Image reference (repeatable)
15
+ --model <model> Builder model (gpt-5.5, gpt-5.4, gpt-5.4-mini)
16
+ --language <ko|en|both> Preferred output language hint
17
+ --server <url> Override server URL
18
+ --json Output raw JSON
19
+ -h, --help Show this help
20
+
21
+ Examples:
22
+ ima2 prompt build --message "make this prompt more cinematic" --json
23
+ ima2 prompt build --messages @conversation.json --model gpt-5.5
24
+ echo '{"role":"user","content":"hi"}' | ima2 prompt build --messages -
25
+ `;
26
+ const FLAGS = {
27
+ message: { type: "string" },
28
+ messages: { type: "string" },
29
+ ref: { type: "string", repeatable: true },
30
+ model: { type: "string" },
31
+ language: { type: "string" },
32
+ server: { type: "string" },
33
+ json: { type: "boolean" },
34
+ help: { short: "h", type: "boolean" },
35
+ };
36
+ async function resolveMessages(args) {
37
+ if (args.messages) {
38
+ const raw = String(args.messages);
39
+ let text;
40
+ if (raw === "-") {
41
+ text = await readStdin();
42
+ }
43
+ else if (raw.startsWith("@")) {
44
+ text = await readFile(raw.slice(1), "utf-8");
45
+ }
46
+ else {
47
+ text = await readFile(raw, "utf-8");
48
+ }
49
+ const parsed = JSON.parse(text);
50
+ if (!Array.isArray(parsed))
51
+ die(2, "--messages must resolve to a JSON array");
52
+ return parsed;
53
+ }
54
+ if (args.message) {
55
+ return [{ role: "user", content: String(args.message) }];
56
+ }
57
+ die(2, "--message or --messages required");
58
+ throw new Error("unreachable");
59
+ }
60
+ export default async function buildSub(argv) {
61
+ const args = parseArgs(argv, { flags: FLAGS });
62
+ if (args.help) {
63
+ out(BUILD_HELP);
64
+ return;
65
+ }
66
+ const messages = await resolveMessages(args);
67
+ const body = { messages };
68
+ if (args.model)
69
+ body.model = args.model;
70
+ let server;
71
+ try {
72
+ server = await resolveServer({ serverFlag: args.server });
73
+ }
74
+ catch (e) {
75
+ die(exitCodeForError(e), e.message);
76
+ throw e;
77
+ }
78
+ let result;
79
+ try {
80
+ result = await request(server.base, "/api/prompt-builder/chat", {
81
+ method: "POST",
82
+ body,
83
+ });
84
+ }
85
+ catch (e) {
86
+ const err = e;
87
+ die(exitCodeForError(e), `${err.message}${err.code ? ` (${err.code})` : ""}`);
88
+ throw e;
89
+ }
90
+ if (args.json) {
91
+ json(result);
92
+ return;
93
+ }
94
+ const message = result.message;
95
+ const content = message?.content ?? "";
96
+ if (content) {
97
+ out(content);
98
+ }
99
+ else {
100
+ out(color.dim("(empty response)"));
101
+ }
102
+ }
@@ -0,0 +1,101 @@
1
+ import { readFile } from "fs/promises";
2
+ import { parseArgs } from "../../lib/args.js";
3
+ import { resolveServer, request } from "../../lib/client.js";
4
+ import { readStdin } from "../../lib/files.js";
5
+ import { out, die, color, json, exitCodeForError } from "../../lib/output.js";
6
+
7
+ const BUILD_HELP = `
8
+ ima2 prompt build [options]
9
+
10
+ Refine prompt intent through the embedded prompt builder.
11
+
12
+ Options:
13
+ --message <text> User message (required unless --messages)
14
+ --messages <file|@file|-> Multi-turn conversation as JSON array
15
+ --ref <image> Image reference (repeatable)
16
+ --model <model> Builder model (gpt-5.5, gpt-5.4, gpt-5.4-mini)
17
+ --language <ko|en|both> Preferred output language hint
18
+ --server <url> Override server URL
19
+ --json Output raw JSON
20
+ -h, --help Show this help
21
+
22
+ Examples:
23
+ ima2 prompt build --message "make this prompt more cinematic" --json
24
+ ima2 prompt build --messages @conversation.json --model gpt-5.5
25
+ echo '{"role":"user","content":"hi"}' | ima2 prompt build --messages -
26
+ `;
27
+
28
+ const FLAGS = {
29
+ message: { type: "string" },
30
+ messages: { type: "string" },
31
+ ref: { type: "string", repeatable: true },
32
+ model: { type: "string" },
33
+ language: { type: "string" },
34
+ server: { type: "string" },
35
+ json: { type: "boolean" },
36
+ help: { short: "h", type: "boolean" },
37
+ } as const;
38
+
39
+ async function resolveMessages(args: Record<string, unknown>): Promise<Array<{ role: string; content: string }>> {
40
+ if (args.messages) {
41
+ const raw = String(args.messages);
42
+ let text: string;
43
+ if (raw === "-") {
44
+ text = await readStdin();
45
+ } else if (raw.startsWith("@")) {
46
+ text = await readFile(raw.slice(1), "utf-8");
47
+ } else {
48
+ text = await readFile(raw, "utf-8");
49
+ }
50
+ const parsed = JSON.parse(text);
51
+ if (!Array.isArray(parsed)) die(2, "--messages must resolve to a JSON array");
52
+ return parsed;
53
+ }
54
+ if (args.message) {
55
+ return [{ role: "user", content: String(args.message) }];
56
+ }
57
+ die(2, "--message or --messages required");
58
+ throw new Error("unreachable");
59
+ }
60
+
61
+ export default async function buildSub(argv: string[]) {
62
+ const args = parseArgs(argv, { flags: FLAGS });
63
+ if (args.help) { out(BUILD_HELP); return; }
64
+
65
+ const messages = await resolveMessages(args);
66
+ const body: Record<string, unknown> = { messages };
67
+ if (args.model) body.model = args.model;
68
+
69
+ let server: { base: string };
70
+ try {
71
+ server = await resolveServer({ serverFlag: args.server as string | undefined });
72
+ } catch (e: unknown) {
73
+ die(exitCodeForError(e), (e as Error).message);
74
+ throw e;
75
+ }
76
+
77
+ let result: Record<string, unknown>;
78
+ try {
79
+ result = await request(server.base, "/api/prompt-builder/chat", {
80
+ method: "POST",
81
+ body,
82
+ }) as Record<string, unknown>;
83
+ } catch (e: unknown) {
84
+ const err = e as { message?: string; code?: string };
85
+ die(exitCodeForError(e), `${err.message}${err.code ? ` (${err.code})` : ""}`);
86
+ throw e;
87
+ }
88
+
89
+ if (args.json) {
90
+ json(result);
91
+ return;
92
+ }
93
+
94
+ const message = result.message as { content?: string } | undefined;
95
+ const content = message?.content ?? "";
96
+ if (content) {
97
+ out(content);
98
+ } else {
99
+ out(color.dim("(empty response)"));
100
+ }
101
+ }
@@ -15,6 +15,10 @@ const HELP = `
15
15
  favorite <id>
16
16
  export [-o <file>] Dump all non-trash prompts + folders
17
17
 
18
+ Builder:
19
+ build --message "<text>" [--ref <image>] [--model <m>] [--json]
20
+ build --messages <file|@file|-> [--json]
21
+
18
22
  Folders:
19
23
  folder ls
20
24
  folder create <name>
@@ -27,6 +31,8 @@ const HELP = `
27
31
  import curated --source <id> [-q <q>] [--limit <n>] [--folder <id>] [--dry-run]
28
32
  import discovery -q <q> --seed <repo>... [--limit <n>] [--folder <id>] [--dry-run]
29
33
  import folder <path> [--folder <id>] [--dry-run]
34
+ import json <file|@file|-> [--folder <id>] [--dry-run]
35
+ import preview <file|@file|-> [--filename <name>] [--json]
30
36
 
31
37
  Common options:
32
38
  --server <url> Override server URL
@@ -41,6 +47,7 @@ const COMMON_FLAGS = {
41
47
  text: { type: "string" },
42
48
  name: { type: "string" },
43
49
  folder: { type: "string" },
50
+ filename: { type: "string" },
44
51
  tag: { type: "string", repeatable: true },
45
52
  mode: { type: "string" },
46
53
  search: { type: "string" },
@@ -95,6 +102,14 @@ async function resolveText(value) {
95
102
  return await readFile(value.slice(1), "utf-8");
96
103
  return value;
97
104
  }
105
+ async function readSourceArg(value, fallbackFilename) {
106
+ if (!value || typeof value !== "string")
107
+ die(2, "source file required");
108
+ if (value === "-")
109
+ return { text: await readStdin(), filename: fallbackFilename };
110
+ const path = value.startsWith("@") ? value.slice(1) : value;
111
+ return { text: await readFile(path, "utf-8"), filename: path.split(/[\\/]/).pop() || fallbackFilename };
112
+ }
98
113
  // ---------- core ----------
99
114
  async function lsSub(argv) {
100
115
  const args = parseArgs(argv, { flags: COMMON_FLAGS });
@@ -331,7 +346,11 @@ async function importSub(argv) {
331
346
  return importDiscovery(rest);
332
347
  if (action === "folder")
333
348
  return importFolder(rest);
334
- die(2, "usage: prompt import <sources|refresh|curated|discovery|folder> ...");
349
+ if (action === "json")
350
+ return importJson(rest);
351
+ if (action === "preview")
352
+ return importPreview(rest);
353
+ die(2, "usage: prompt import <sources|refresh|curated|discovery|folder|json|preview> ...");
335
354
  }
336
355
  async function importSources(argv) {
337
356
  const args = parseArgs(argv, { flags: COMMON_FLAGS });
@@ -485,6 +504,59 @@ async function importFolder(argv) {
485
504
  }
486
505
  out(color.green(`✓ imported ${commit.imported || candidates.length}`));
487
506
  }
507
+ async function importJson(argv) {
508
+ const args = parseArgs(argv, { flags: COMMON_FLAGS });
509
+ const { text } = await readSourceArg(args.positional[0], "prompts.json");
510
+ const parsed = JSON.parse(text);
511
+ const body = Array.isArray(parsed) ? { prompts: parsed } : { ...parsed };
512
+ if (args.folder && Array.isArray(body.prompts)) {
513
+ body.prompts = body.prompts.map((prompt) => (prompt && typeof prompt === "object"
514
+ ? { ...prompt, folderId: args.folder }
515
+ : prompt));
516
+ }
517
+ if (args["dry-run"]) {
518
+ json({
519
+ folders: Array.isArray(body.folders) ? body.folders.length : 0,
520
+ prompts: Array.isArray(body.prompts) ? body.prompts.length : 0,
521
+ });
522
+ return;
523
+ }
524
+ const server = await getServer(args);
525
+ const resp = await request(server.base, "/api/prompts/import", {
526
+ method: "POST",
527
+ body,
528
+ }).catch(handle);
529
+ if (args.json) {
530
+ json(resp);
531
+ return;
532
+ }
533
+ out(color.green("✓ imported prompt JSON"));
534
+ out(JSON.stringify(resp, null, 2));
535
+ }
536
+ async function importPreview(argv) {
537
+ const args = parseArgs(argv, { flags: COMMON_FLAGS });
538
+ const { text, filename } = await readSourceArg(args.positional[0], "prompt-source.md");
539
+ const server = await getServer(args);
540
+ const resp = await request(server.base, "/api/prompts/import/preview", {
541
+ method: "POST",
542
+ body: {
543
+ source: {
544
+ kind: "local",
545
+ filename: args.filename || filename,
546
+ text,
547
+ },
548
+ },
549
+ }).catch(handle);
550
+ if (args.json) {
551
+ json(resp);
552
+ return;
553
+ }
554
+ out(JSON.stringify(resp, null, 2));
555
+ }
556
+ async function buildSub(argv) {
557
+ const mod = await import("./prompt-sub/build.js");
558
+ return mod.default(argv);
559
+ }
488
560
  const SUB = {
489
561
  ls: lsSub,
490
562
  show: showSub,
@@ -495,6 +567,7 @@ const SUB = {
495
567
  export: exportSub,
496
568
  folder: folderSub,
497
569
  import: importSub,
570
+ build: buildSub,
498
571
  };
499
572
  export default async function promptCmd(argv) {
500
573
  const sub = argv[0];
@@ -16,6 +16,10 @@ const HELP = `
16
16
  favorite <id>
17
17
  export [-o <file>] Dump all non-trash prompts + folders
18
18
 
19
+ Builder:
20
+ build --message "<text>" [--ref <image>] [--model <m>] [--json]
21
+ build --messages <file|@file|-> [--json]
22
+
19
23
  Folders:
20
24
  folder ls
21
25
  folder create <name>
@@ -28,6 +32,8 @@ const HELP = `
28
32
  import curated --source <id> [-q <q>] [--limit <n>] [--folder <id>] [--dry-run]
29
33
  import discovery -q <q> --seed <repo>... [--limit <n>] [--folder <id>] [--dry-run]
30
34
  import folder <path> [--folder <id>] [--dry-run]
35
+ import json <file|@file|-> [--folder <id>] [--dry-run]
36
+ import preview <file|@file|-> [--filename <name>] [--json]
31
37
 
32
38
  Common options:
33
39
  --server <url> Override server URL
@@ -43,6 +49,7 @@ const COMMON_FLAGS = {
43
49
  text: { type: "string" },
44
50
  name: { type: "string" },
45
51
  folder: { type: "string" },
52
+ filename: { type: "string" },
46
53
  tag: { type: "string", repeatable: true },
47
54
  mode: { type: "string" },
48
55
  search: { type: "string" },
@@ -93,6 +100,13 @@ async function resolveText(value: unknown): Promise<string | null> {
93
100
  return value;
94
101
  }
95
102
 
103
+ async function readSourceArg(value: unknown, fallbackFilename: string) {
104
+ if (!value || typeof value !== "string") die(2, "source file required");
105
+ if (value === "-") return { text: await readStdin(), filename: fallbackFilename };
106
+ const path = value.startsWith("@") ? value.slice(1) : value;
107
+ return { text: await readFile(path, "utf-8"), filename: path.split(/[\\/]/).pop() || fallbackFilename };
108
+ }
109
+
96
110
  // ---------- core ----------
97
111
 
98
112
  async function lsSub(argv: string[]) {
@@ -284,7 +298,9 @@ async function importSub(argv: string[]) {
284
298
  if (action === "curated") return importCurated(rest);
285
299
  if (action === "discovery") return importDiscovery(rest);
286
300
  if (action === "folder") return importFolder(rest);
287
- die(2, "usage: prompt import <sources|refresh|curated|discovery|folder> ...");
301
+ if (action === "json") return importJson(rest);
302
+ if (action === "preview") return importPreview(rest);
303
+ die(2, "usage: prompt import <sources|refresh|curated|discovery|folder|json|preview> ...");
288
304
  }
289
305
 
290
306
  async function importSources(argv: string[]) {
@@ -402,6 +418,58 @@ async function importFolder(argv: string[]) {
402
418
  out(color.green(`✓ imported ${commit.imported || candidates.length}`));
403
419
  }
404
420
 
421
+ async function importJson(argv: string[]) {
422
+ const args = parseArgs(argv, { flags: COMMON_FLAGS });
423
+ const { text } = await readSourceArg(args.positional[0], "prompts.json");
424
+ const parsed = JSON.parse(text) as { folders?: unknown; prompts?: unknown } | unknown[];
425
+ const body = Array.isArray(parsed) ? { prompts: parsed } : { ...parsed };
426
+ if (args.folder && Array.isArray(body.prompts)) {
427
+ body.prompts = body.prompts.map((prompt) => (
428
+ prompt && typeof prompt === "object"
429
+ ? { ...(prompt as Record<string, unknown>), folderId: args.folder }
430
+ : prompt
431
+ ));
432
+ }
433
+ if (args["dry-run"]) {
434
+ json({
435
+ folders: Array.isArray(body.folders) ? body.folders.length : 0,
436
+ prompts: Array.isArray(body.prompts) ? body.prompts.length : 0,
437
+ });
438
+ return;
439
+ }
440
+ const server = await getServer(args);
441
+ const resp = await request(server.base, "/api/prompts/import", {
442
+ method: "POST",
443
+ body,
444
+ }).catch(handle);
445
+ if (args.json) { json(resp); return; }
446
+ out(color.green("✓ imported prompt JSON"));
447
+ out(JSON.stringify(resp, null, 2));
448
+ }
449
+
450
+ async function importPreview(argv: string[]) {
451
+ const args = parseArgs(argv, { flags: COMMON_FLAGS });
452
+ const { text, filename } = await readSourceArg(args.positional[0], "prompt-source.md");
453
+ const server = await getServer(args);
454
+ const resp = await request(server.base, "/api/prompts/import/preview", {
455
+ method: "POST",
456
+ body: {
457
+ source: {
458
+ kind: "local",
459
+ filename: args.filename || filename,
460
+ text,
461
+ },
462
+ },
463
+ }).catch(handle);
464
+ if (args.json) { json(resp); return; }
465
+ out(JSON.stringify(resp, null, 2));
466
+ }
467
+
468
+ async function buildSub(argv: string[]) {
469
+ const mod = await import("./prompt-sub/build.js");
470
+ return mod.default(argv);
471
+ }
472
+
405
473
  const SUB: Record<string, (argv: any[]) => Promise<void>> = {
406
474
  ls: lsSub,
407
475
  show: showSub,
@@ -412,6 +480,7 @@ const SUB: Record<string, (argv: any[]) => Promise<void>> = {
412
480
  export: exportSub,
413
481
  folder: folderSub,
414
482
  import: importSub,
483
+ build: buildSub,
415
484
  };
416
485
 
417
486
  export default async function promptCmd(argv: string[]) {