helloloop 0.3.1 → 0.6.1

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 (53) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +3 -3
  3. package/README.md +157 -81
  4. package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
  5. package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +13 -10
  6. package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +9 -4
  7. package/hosts/gemini/extension/GEMINI.md +12 -7
  8. package/hosts/gemini/extension/commands/helloloop.toml +14 -10
  9. package/hosts/gemini/extension/gemini-extension.json +1 -1
  10. package/package.json +2 -2
  11. package/skills/helloloop/SKILL.md +16 -6
  12. package/src/analyze_confirmation.mjs +29 -5
  13. package/src/analyze_prompt.mjs +5 -1
  14. package/src/analyze_user_input.mjs +20 -2
  15. package/src/analyzer.mjs +130 -43
  16. package/src/cli.mjs +32 -492
  17. package/src/cli_analyze_command.mjs +248 -0
  18. package/src/cli_args.mjs +106 -0
  19. package/src/cli_command_handlers.mjs +120 -0
  20. package/src/cli_context.mjs +31 -0
  21. package/src/cli_render.mjs +70 -0
  22. package/src/cli_support.mjs +11 -14
  23. package/src/completion_review.mjs +243 -0
  24. package/src/config.mjs +50 -0
  25. package/src/discovery_prompt.mjs +2 -27
  26. package/src/engine_metadata.mjs +79 -0
  27. package/src/engine_selection.mjs +335 -0
  28. package/src/engine_selection_failure.mjs +51 -0
  29. package/src/engine_selection_messages.mjs +119 -0
  30. package/src/engine_selection_probe.mjs +78 -0
  31. package/src/engine_selection_prompt.mjs +48 -0
  32. package/src/engine_selection_settings.mjs +38 -0
  33. package/src/guardrails.mjs +15 -4
  34. package/src/install.mjs +6 -405
  35. package/src/install_claude.mjs +189 -0
  36. package/src/install_codex.mjs +114 -0
  37. package/src/install_gemini.mjs +43 -0
  38. package/src/install_shared.mjs +90 -0
  39. package/src/process.mjs +482 -39
  40. package/src/prompt.mjs +9 -5
  41. package/src/prompt_session.mjs +40 -0
  42. package/src/runner.mjs +3 -341
  43. package/src/runner_execute_task.mjs +301 -0
  44. package/src/runner_execution_support.mjs +155 -0
  45. package/src/runner_loop.mjs +106 -0
  46. package/src/runner_once.mjs +29 -0
  47. package/src/runner_status.mjs +104 -0
  48. package/src/runtime_recovery.mjs +301 -0
  49. package/src/shell_invocation.mjs +16 -0
  50. package/templates/analysis-output.schema.json +0 -1
  51. package/templates/policy.template.json +27 -0
  52. package/templates/project.template.json +2 -0
  53. package/templates/task-review-output.schema.json +70 -0
@@ -0,0 +1,114 @@
1
+ import path from "node:path";
2
+
3
+ import { ensureDir, fileExists, readJson, writeJson } from "./common.mjs";
4
+ import {
5
+ assertPathInside,
6
+ codexBundleEntries,
7
+ copyBundleEntries,
8
+ removePathIfExists,
9
+ removeTargetIfNeeded,
10
+ resolveHomeDir,
11
+ } from "./install_shared.mjs";
12
+
13
+ function updateCodexMarketplace(marketplaceFile) {
14
+ const marketplace = fileExists(marketplaceFile)
15
+ ? readJson(marketplaceFile)
16
+ : {
17
+ name: "local-plugins",
18
+ interface: {
19
+ displayName: "Local Plugins",
20
+ },
21
+ plugins: [],
22
+ };
23
+
24
+ marketplace.interface = marketplace.interface || {
25
+ displayName: "Local Plugins",
26
+ };
27
+ marketplace.plugins = Array.isArray(marketplace.plugins) ? marketplace.plugins : [];
28
+
29
+ const nextEntry = {
30
+ name: "helloloop",
31
+ source: {
32
+ source: "local",
33
+ path: "./plugins/helloloop",
34
+ },
35
+ policy: {
36
+ installation: "AVAILABLE",
37
+ authentication: "ON_INSTALL",
38
+ },
39
+ category: "Coding",
40
+ };
41
+
42
+ const existingIndex = marketplace.plugins.findIndex((plugin) => plugin?.name === "helloloop");
43
+ if (existingIndex >= 0) {
44
+ marketplace.plugins.splice(existingIndex, 1, nextEntry);
45
+ } else {
46
+ marketplace.plugins.push(nextEntry);
47
+ }
48
+
49
+ writeJson(marketplaceFile, marketplace);
50
+ }
51
+
52
+ function removeCodexMarketplaceEntry(marketplaceFile) {
53
+ if (!fileExists(marketplaceFile)) {
54
+ return false;
55
+ }
56
+
57
+ const marketplace = readJson(marketplaceFile);
58
+ const plugins = Array.isArray(marketplace.plugins) ? marketplace.plugins : [];
59
+ const nextPlugins = plugins.filter((plugin) => plugin?.name !== "helloloop");
60
+ if (nextPlugins.length === plugins.length) {
61
+ return false;
62
+ }
63
+
64
+ marketplace.plugins = nextPlugins;
65
+ writeJson(marketplaceFile, marketplace);
66
+ return true;
67
+ }
68
+
69
+ export function installCodexHost(bundleRoot, options = {}) {
70
+ const resolvedCodexHome = resolveHomeDir(options.codexHome, ".codex");
71
+ const targetPluginsRoot = path.join(resolvedCodexHome, "plugins");
72
+ const targetPluginRoot = path.join(targetPluginsRoot, "helloloop");
73
+ const marketplaceFile = path.join(resolvedCodexHome, ".agents", "plugins", "marketplace.json");
74
+ const manifestFile = path.join(bundleRoot, ".codex-plugin", "plugin.json");
75
+
76
+ if (!fileExists(manifestFile)) {
77
+ throw new Error(`未找到 Codex 插件 manifest:${manifestFile}`);
78
+ }
79
+
80
+ assertPathInside(resolvedCodexHome, targetPluginRoot, "Codex 目标插件目录");
81
+ removeTargetIfNeeded(targetPluginRoot, options.force);
82
+
83
+ ensureDir(targetPluginsRoot);
84
+ ensureDir(targetPluginRoot);
85
+ copyBundleEntries(bundleRoot, targetPluginRoot, codexBundleEntries);
86
+ removePathIfExists(path.join(targetPluginRoot, ".git"));
87
+
88
+ ensureDir(path.dirname(marketplaceFile));
89
+ updateCodexMarketplace(marketplaceFile);
90
+
91
+ return {
92
+ host: "codex",
93
+ displayName: "Codex",
94
+ targetRoot: targetPluginRoot,
95
+ marketplaceFile,
96
+ };
97
+ }
98
+
99
+ export function uninstallCodexHost(options = {}) {
100
+ const resolvedCodexHome = resolveHomeDir(options.codexHome, ".codex");
101
+ const targetPluginRoot = path.join(resolvedCodexHome, "plugins", "helloloop");
102
+ const marketplaceFile = path.join(resolvedCodexHome, ".agents", "plugins", "marketplace.json");
103
+
104
+ const removedPlugin = removePathIfExists(targetPluginRoot);
105
+ const removedMarketplace = removeCodexMarketplaceEntry(marketplaceFile);
106
+
107
+ return {
108
+ host: "codex",
109
+ displayName: "Codex",
110
+ targetRoot: targetPluginRoot,
111
+ removed: removedPlugin || removedMarketplace,
112
+ marketplaceFile,
113
+ };
114
+ }
@@ -0,0 +1,43 @@
1
+ import path from "node:path";
2
+
3
+ import { ensureDir, fileExists } from "./common.mjs";
4
+ import {
5
+ assertPathInside,
6
+ copyDirectory,
7
+ removePathIfExists,
8
+ removeTargetIfNeeded,
9
+ resolveHomeDir,
10
+ } from "./install_shared.mjs";
11
+
12
+ export function installGeminiHost(bundleRoot, options = {}) {
13
+ const resolvedGeminiHome = resolveHomeDir(options.geminiHome, ".gemini");
14
+ const sourceExtensionRoot = path.join(bundleRoot, "hosts", "gemini", "extension");
15
+ const targetExtensionRoot = path.join(resolvedGeminiHome, "extensions", "helloloop");
16
+
17
+ if (!fileExists(path.join(sourceExtensionRoot, "gemini-extension.json"))) {
18
+ throw new Error(`未找到 Gemini 扩展清单:${sourceExtensionRoot}`);
19
+ }
20
+
21
+ assertPathInside(resolvedGeminiHome, targetExtensionRoot, "Gemini 扩展目录");
22
+ removeTargetIfNeeded(targetExtensionRoot, options.force);
23
+ ensureDir(path.dirname(targetExtensionRoot));
24
+ copyDirectory(sourceExtensionRoot, targetExtensionRoot);
25
+
26
+ return {
27
+ host: "gemini",
28
+ displayName: "Gemini",
29
+ targetRoot: targetExtensionRoot,
30
+ };
31
+ }
32
+
33
+ export function uninstallGeminiHost(options = {}) {
34
+ const resolvedGeminiHome = resolveHomeDir(options.geminiHome, ".gemini");
35
+ const targetExtensionRoot = path.join(resolvedGeminiHome, "extensions", "helloloop");
36
+
37
+ return {
38
+ host: "gemini",
39
+ displayName: "Gemini",
40
+ targetRoot: targetExtensionRoot,
41
+ removed: removePathIfExists(targetExtensionRoot),
42
+ };
43
+ }
@@ -0,0 +1,90 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ import { ensureDir, fileExists, readJson, writeJson } from "./common.mjs";
6
+
7
+ export const runtimeBundleEntries = [
8
+ ".claude-plugin",
9
+ ".codex-plugin",
10
+ "LICENSE",
11
+ "README.md",
12
+ "bin",
13
+ "hosts",
14
+ "package.json",
15
+ "scripts",
16
+ "skills",
17
+ "src",
18
+ "templates",
19
+ ];
20
+
21
+ export const codexBundleEntries = runtimeBundleEntries.filter((entry) => ![
22
+ ".claude-plugin",
23
+ "hosts",
24
+ ].includes(entry));
25
+
26
+ export const supportedHosts = ["codex", "claude", "gemini"];
27
+ export const CLAUDE_MARKETPLACE_NAME = "helloloop-local";
28
+ export const CLAUDE_PLUGIN_KEY = "helloloop@helloloop-local";
29
+
30
+ export function resolveHomeDir(homeDir, defaultDirName) {
31
+ return path.resolve(homeDir || path.join(os.homedir(), defaultDirName));
32
+ }
33
+
34
+ export function assertPathInside(parentDir, targetDir, label) {
35
+ const relative = path.relative(parentDir, targetDir);
36
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
37
+ throw new Error(`${label} 超出允许范围:${targetDir}`);
38
+ }
39
+ }
40
+
41
+ export function removeTargetIfNeeded(targetPath, force) {
42
+ if (!fileExists(targetPath)) {
43
+ return;
44
+ }
45
+ if (!force) {
46
+ throw new Error(`目标目录已存在:${targetPath}。若要覆盖,请追加 --force。`);
47
+ }
48
+ fs.rmSync(targetPath, { recursive: true, force: true });
49
+ }
50
+
51
+ export function removePathIfExists(targetPath) {
52
+ if (!fileExists(targetPath)) {
53
+ return false;
54
+ }
55
+ fs.rmSync(targetPath, { recursive: true, force: true });
56
+ return true;
57
+ }
58
+
59
+ export function copyBundleEntries(bundleRoot, targetRoot, entries) {
60
+ for (const entry of entries) {
61
+ const sourcePath = path.join(bundleRoot, entry);
62
+ if (!fileExists(sourcePath)) {
63
+ continue;
64
+ }
65
+
66
+ fs.cpSync(sourcePath, path.join(targetRoot, entry), {
67
+ force: true,
68
+ recursive: true,
69
+ });
70
+ }
71
+ }
72
+
73
+ export function copyDirectory(sourceRoot, targetRoot) {
74
+ fs.cpSync(sourceRoot, targetRoot, {
75
+ force: true,
76
+ recursive: true,
77
+ });
78
+ }
79
+
80
+ export function loadOrInitJson(filePath, fallbackValue) {
81
+ if (!fileExists(filePath)) {
82
+ return fallbackValue;
83
+ }
84
+ return readJson(filePath);
85
+ }
86
+
87
+ export function writeJsonFile(filePath, value) {
88
+ ensureDir(path.dirname(filePath));
89
+ writeJson(filePath, value);
90
+ }