@hybridlabor-api/aos 4.8.0 → 4.9.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 (166) hide show
  1. package/.agents/agents.md +1 -1
  2. package/.agents/nodes.json +1 -1
  3. package/.agents/vendor-manifest.json +23 -1
  4. package/.claude/agents/godmode-media-eventtech.md +1 -1
  5. package/.claude/hooks/conventional-commits.mjs +125 -0
  6. package/.claude/hooks/env-file-protection.mjs +105 -0
  7. package/.claude/hooks/go-gate.mjs +101 -81
  8. package/.claude/settings.json +13 -0
  9. package/.opencode/agents/godmode-media-eventtech.md +1 -1
  10. package/.opencode/plugins/bdb-aos.js +31 -4
  11. package/README.de.md +1 -1
  12. package/README.md +1 -1
  13. package/README.pt.md +1 -1
  14. package/THIRD_PARTY_NOTICES.md +88 -0
  15. package/docs/skills_table.md +1 -1
  16. package/installer.js +174 -46
  17. package/package.json +6 -2
  18. package/packages/aos-cli/README.md +80 -0
  19. package/packages/aos-cli/bin/aos-cli.mjs +134 -0
  20. package/packages/aos-cli/core-skills.json +12 -0
  21. package/packages/aos-cli/extensions/aos.ts +321 -0
  22. package/packages/aos-cli/package-lock.json +1923 -0
  23. package/packages/aos-cli/package.json +29 -0
  24. package/packages/aos-cli/scripts/check-theme.mjs +63 -0
  25. package/packages/aos-cli/themes/aos.json +97 -0
  26. package/scripts/build-plugin-manifest.mjs +131 -0
  27. package/scripts/validate-skills.mjs +81 -6
  28. package/skills/basic/ao-orchestrator/SKILL.md +116 -0
  29. package/skills/global_config/agenttrail/SKILL.md +6 -1
  30. package/skills/global_config/ask-tim/SKILL.md +4 -4
  31. package/skills/global_config/bash-script-generator/SKILL.md +201 -0
  32. package/skills/global_config/bash-script-generator/assets/templates/standard-template.sh +96 -0
  33. package/skills/global_config/bash-script-generator/docs/bash-scripting-guide.md +729 -0
  34. package/skills/global_config/bash-script-generator/docs/generation-best-practices.md +193 -0
  35. package/skills/global_config/bash-script-generator/docs/script-patterns.md +566 -0
  36. package/skills/global_config/bash-script-generator/docs/text-processing-guide.md +437 -0
  37. package/skills/global_config/bash-script-generator/examples/log-analyzer.sh +92 -0
  38. package/skills/global_config/bash-script-generator/scripts/generate_script_template.sh +123 -0
  39. package/skills/global_config/bash-script-generator/scripts/run_ci_checks.sh +172 -0
  40. package/skills/global_config/bash-script-generator/scripts/test_generator.sh +413 -0
  41. package/skills/global_config/bash-script-validator/SKILL.md +249 -0
  42. package/skills/global_config/bash-script-validator/docs/awk-reference.md +449 -0
  43. package/skills/global_config/bash-script-validator/docs/bash-reference.md +468 -0
  44. package/skills/global_config/bash-script-validator/docs/common-mistakes.md +623 -0
  45. package/skills/global_config/bash-script-validator/docs/grep-reference.md +395 -0
  46. package/skills/global_config/bash-script-validator/docs/regex-reference.md +391 -0
  47. package/skills/global_config/bash-script-validator/docs/sed-reference.md +454 -0
  48. package/skills/global_config/bash-script-validator/docs/shell-reference.md +463 -0
  49. package/skills/global_config/bash-script-validator/docs/shellcheck-reference.md +399 -0
  50. package/skills/global_config/bash-script-validator/examples/bad-bash.sh +55 -0
  51. package/skills/global_config/bash-script-validator/examples/bad-shell.sh +54 -0
  52. package/skills/global_config/bash-script-validator/examples/good-bash.sh +71 -0
  53. package/skills/global_config/bash-script-validator/examples/good-shell.sh +69 -0
  54. package/skills/global_config/bash-script-validator/scripts/run_ci_checks.sh +23 -0
  55. package/skills/global_config/bash-script-validator/scripts/shellcheck_wrapper.sh +174 -0
  56. package/skills/global_config/bash-script-validator/scripts/test_validate.sh +446 -0
  57. package/skills/global_config/bash-script-validator/scripts/validate.sh +512 -0
  58. package/skills/global_config/ci-pipeline/SKILL.md +135 -0
  59. package/skills/global_config/dispatching-parallel-agents/SKILL.md +170 -0
  60. package/skills/global_config/dockerfile-generator/SKILL.md +1038 -0
  61. package/skills/global_config/dockerfile-generator/examples/example.dockerignore +95 -0
  62. package/skills/global_config/dockerfile-generator/examples/golang-distroless.Dockerfile +34 -0
  63. package/skills/global_config/dockerfile-generator/examples/java-springboot.Dockerfile +45 -0
  64. package/skills/global_config/dockerfile-generator/examples/nextjs-production.Dockerfile +49 -0
  65. package/skills/global_config/dockerfile-generator/examples/nodejs-multistage.Dockerfile +55 -0
  66. package/skills/global_config/dockerfile-generator/examples/python-fastapi.Dockerfile +48 -0
  67. package/skills/global_config/dockerfile-generator/references/language_specific_guides.md +510 -0
  68. package/skills/global_config/dockerfile-generator/references/multistage_builds.md +570 -0
  69. package/skills/global_config/dockerfile-generator/references/optimization_patterns.md +492 -0
  70. package/skills/global_config/dockerfile-generator/references/security_best_practices.md +375 -0
  71. package/skills/global_config/dockerfile-generator/scripts/generate_dockerignore.sh +199 -0
  72. package/skills/global_config/dockerfile-generator/scripts/generate_golang.sh +172 -0
  73. package/skills/global_config/dockerfile-generator/scripts/generate_java.sh +185 -0
  74. package/skills/global_config/dockerfile-generator/scripts/generate_nodejs.sh +279 -0
  75. package/skills/global_config/dockerfile-generator/scripts/generate_python.sh +218 -0
  76. package/skills/global_config/dockerfile-generator/scripts/test_generator.sh +210 -0
  77. package/skills/global_config/dockerfile-validator/SKILL.md +300 -0
  78. package/skills/global_config/dockerfile-validator/examples/.dockerignore.example +34 -0
  79. package/skills/global_config/dockerfile-validator/examples/bad-example.Dockerfile +37 -0
  80. package/skills/global_config/dockerfile-validator/examples/golang-distroless.Dockerfile +50 -0
  81. package/skills/global_config/dockerfile-validator/examples/good-example.Dockerfile +52 -0
  82. package/skills/global_config/dockerfile-validator/examples/python-optimized.Dockerfile +53 -0
  83. package/skills/global_config/dockerfile-validator/examples/security-issues.Dockerfile +42 -0
  84. package/skills/global_config/dockerfile-validator/references/docker_best_practices.md +348 -0
  85. package/skills/global_config/dockerfile-validator/references/optimization_guide.md +473 -0
  86. package/skills/global_config/dockerfile-validator/references/security_checklist.md +208 -0
  87. package/skills/global_config/dockerfile-validator/scripts/dockerfile-validate.sh +699 -0
  88. package/skills/global_config/dockerfile-validator/scripts/test_validate.sh +35 -0
  89. package/skills/global_config/dockerfile-validator/tests/fixtures/copy-before-yarn-lock-read.Dockerfile +6 -0
  90. package/skills/global_config/dockerfile-validator/tests/fixtures/copy-before-yarn.Dockerfile +6 -0
  91. package/skills/global_config/dockerfile-validator/tests/fixtures/from-platform-nonroot.Dockerfile +7 -0
  92. package/skills/global_config/dockerfile-validator/tests/test_regression.sh +164 -0
  93. package/skills/global_config/finishing-a-development-branch/SKILL.md +228 -0
  94. package/skills/global_config/github-actions-generator/SKILL.md +353 -0
  95. package/skills/global_config/github-actions-generator/assets/templates/action/composite/action.yml +82 -0
  96. package/skills/global_config/github-actions-generator/assets/templates/action/docker/Dockerfile +25 -0
  97. package/skills/global_config/github-actions-generator/assets/templates/action/docker/action.yml +42 -0
  98. package/skills/global_config/github-actions-generator/assets/templates/action/docker/entrypoint.sh +27 -0
  99. package/skills/global_config/github-actions-generator/assets/templates/action/javascript/action.yml +33 -0
  100. package/skills/global_config/github-actions-generator/assets/templates/action/javascript/index.js +50 -0
  101. package/skills/global_config/github-actions-generator/assets/templates/action/javascript/package.json +27 -0
  102. package/skills/global_config/github-actions-generator/assets/templates/workflow/basic_workflow.yml +242 -0
  103. package/skills/global_config/github-actions-generator/assets/templates/workflow/reusable_workflow.yml +106 -0
  104. package/skills/global_config/github-actions-generator/examples/README.md +147 -0
  105. package/skills/global_config/github-actions-generator/examples/actions/setup-node-cached/action.yml +93 -0
  106. package/skills/global_config/github-actions-generator/examples/caching/docker-buildkit.yml +256 -0
  107. package/skills/global_config/github-actions-generator/examples/security/dependency-review.yml +62 -0
  108. package/skills/global_config/github-actions-generator/examples/security/sbom-attestation.yml +119 -0
  109. package/skills/global_config/github-actions-generator/examples/triggers/chatops-commands.yml +475 -0
  110. package/skills/global_config/github-actions-generator/examples/triggers/repository-dispatch.yml +418 -0
  111. package/skills/global_config/github-actions-generator/examples/triggers/workflow-orchestration.yml +404 -0
  112. package/skills/global_config/github-actions-generator/examples/workflows/docker-build-push.yml +68 -0
  113. package/skills/global_config/github-actions-generator/examples/workflows/go-ci.yml +161 -0
  114. package/skills/global_config/github-actions-generator/examples/workflows/monorepo-ci.yml +340 -0
  115. package/skills/global_config/github-actions-generator/examples/workflows/multi-environment-deploy.yml +406 -0
  116. package/skills/global_config/github-actions-generator/examples/workflows/nodejs-ci.yml +122 -0
  117. package/skills/global_config/github-actions-generator/examples/workflows/python-ci.yml +157 -0
  118. package/skills/global_config/github-actions-generator/examples/workflows/scheduled-tasks.yml +376 -0
  119. package/skills/global_config/github-actions-generator/references/advanced-triggers.md +917 -0
  120. package/skills/global_config/github-actions-generator/references/best-practices.md +755 -0
  121. package/skills/global_config/github-actions-generator/references/common-actions.md +715 -0
  122. package/skills/global_config/github-actions-generator/references/custom-actions.md +320 -0
  123. package/skills/global_config/github-actions-generator/references/expressions-and-contexts.md +688 -0
  124. package/skills/global_config/github-actions-generator/references/modern-features.md +421 -0
  125. package/skills/global_config/github-actions-generator/scripts/test_generator.sh +344 -0
  126. package/skills/global_config/github-actions-templates/SKILL.md +7 -0
  127. package/skills/global_config/github-actions-validator/SKILL.md +576 -0
  128. package/skills/global_config/github-actions-validator/examples/README.md +88 -0
  129. package/skills/global_config/github-actions-validator/examples/outdated-versions.yml +76 -0
  130. package/skills/global_config/github-actions-validator/examples/valid-ci.yml +79 -0
  131. package/skills/global_config/github-actions-validator/examples/with-errors.yml +47 -0
  132. package/skills/global_config/github-actions-validator/references/act_usage.md +233 -0
  133. package/skills/global_config/github-actions-validator/references/action_versions.md +122 -0
  134. package/skills/global_config/github-actions-validator/references/actionlint_usage.md +343 -0
  135. package/skills/global_config/github-actions-validator/references/common_errors.md +512 -0
  136. package/skills/global_config/github-actions-validator/references/modern_features.md +384 -0
  137. package/skills/global_config/github-actions-validator/references/runners.md +317 -0
  138. package/skills/global_config/github-actions-validator/scripts/install_tools.sh +113 -0
  139. package/skills/global_config/github-actions-validator/scripts/validate_workflow.sh +910 -0
  140. package/skills/global_config/github-actions-validator/tests/test_validate_workflow.sh +237 -0
  141. package/skills/global_config/makefile-generator/SKILL.md +614 -0
  142. package/skills/global_config/makefile-generator/assets/templates/.gitkeep +1 -0
  143. package/skills/global_config/makefile-generator/docs/makefile-structure.md +530 -0
  144. package/skills/global_config/makefile-generator/docs/optimization-guide.md +784 -0
  145. package/skills/global_config/makefile-generator/docs/patterns-guide.md +642 -0
  146. package/skills/global_config/makefile-generator/docs/security-guide.md +361 -0
  147. package/skills/global_config/makefile-generator/docs/targets-guide.md +642 -0
  148. package/skills/global_config/makefile-generator/docs/variables-guide.md +596 -0
  149. package/skills/global_config/makefile-generator/scripts/add_standard_targets.sh +539 -0
  150. package/skills/global_config/makefile-generator/scripts/generate_makefile_template.sh +690 -0
  151. package/skills/global_config/makefile-generator/test/test_helper_scripts.sh +190 -0
  152. package/skills/global_config/makefile-validator/SKILL.md +244 -0
  153. package/skills/global_config/makefile-validator/docs/bake-tool.md +1000 -0
  154. package/skills/global_config/makefile-validator/docs/best-practices.md +858 -0
  155. package/skills/global_config/makefile-validator/docs/common-mistakes.md +944 -0
  156. package/skills/global_config/makefile-validator/examples/bad-makefile.mk +77 -0
  157. package/skills/global_config/makefile-validator/examples/good-makefile.mk +103 -0
  158. package/skills/global_config/makefile-validator/scripts/test_validate.sh +382 -0
  159. package/skills/global_config/makefile-validator/scripts/validate_makefile.sh +712 -0
  160. package/skills/global_config/{MCP_Manage → mcp-manage}/SKILL.md +3 -3
  161. package/skills/global_config/requesting-code-review/SKILL.md +98 -0
  162. package/skills/global_config/requesting-code-review/code-reviewer.md +198 -0
  163. package/skills/global_config/using-git-worktrees/SKILL.md +170 -0
  164. package/skills/global_config/verification-before-completion/SKILL.md +123 -0
  165. package/skills/global_config/writing-plans/SKILL.md +126 -46
  166. package/skills/global_config/writing-plans-legacy/SKILL.md +139 -0
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ // AOS CLI -- the AOS dispatcher contract on top of the pi coding agent.
3
+ //
4
+ // What this is: a launcher. Everything it provides already exists --
5
+ // * the AOS skills live in ~/.agents/skills, which the `aos` installer
6
+ // writes and which pi discovers natively (Agent Skills spec);
7
+ // * the 7-node dispatcher graph lives in ~/.agents/AGENTS.md.
8
+ //
9
+ // What this is not: a second copy of the AOS payload. It reads ~/.agents at
10
+ // runtime and never imports @hybridlabor-api/aos, so AOS CLI and the installer
11
+ // version and update independently. That is why pi updates are cheap here and
12
+ // AOS updates are cheap there.
13
+ //
14
+ // aos-cli open the chat in the current directory
15
+ // aos-cli "fix the failing go build" same, with a first prompt
16
+ // aos-cli --continue resume the last session for this project
17
+ // any other flag is handed to pi unchanged
18
+
19
+ import { spawn } from 'node:child_process';
20
+ import { existsSync, readFileSync } from 'node:fs';
21
+ import os from 'node:os';
22
+ import path from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
26
+ const agents = path.join(os.homedir(), '.agents');
27
+
28
+ const args = [
29
+ '--theme', path.join(root, 'themes', 'aos.json'),
30
+ '--use-theme', 'aos',
31
+ '--extension', path.join(root, 'extensions', 'aos.ts'),
32
+ ];
33
+
34
+ // pi discovers AGENTS.md from the working directory and its ancestors only, and
35
+ // ~/.agents/AGENTS.md is neither. Without this flag the dispatcher graph -- the
36
+ // one thing a bare pi genuinely cannot see -- is silently missing.
37
+ const dispatch = path.join(agents, 'AGENTS.md');
38
+ if (existsSync(dispatch)) {
39
+ args.push('--append-system-prompt', dispatch);
40
+ } else {
41
+ console.error('AOS CLI: ~/.agents not found -- run `npx @hybridlabor-api/aos` first.');
42
+ console.error(' Starting without the AOS skills and the dispatcher graph.');
43
+ }
44
+
45
+ // pi advertises every discovered skill by name and description in EVERY system
46
+ // prompt. Measured on this machine: 210 skills, ~11.150 tokens per request, of
47
+ // which the four firecrawl entries alone are ~2.250. That is a permanent tax on
48
+ // every turn to make one skill discoverable.
49
+ //
50
+ // core-skills.json narrows it to the handful worth having always. pi does the
51
+ // work natively: --no-skills stops the discovery walk, and each --skill is added
52
+ // back explicitly. --skill alone would not do it -- it appends to discovery
53
+ // rather than replacing it.
54
+ //
55
+ // The tradeoff is that a skill outside this list is not merely unadvertised but
56
+ // unreachable, not even via /skill:name. `aos-store` is how one gets added.
57
+ const core = coreSkills();
58
+ if (core.length) {
59
+ args.push('--no-skills');
60
+ for (const name of core) args.push('--skill', path.join(agents, 'skills', name));
61
+ }
62
+
63
+ /**
64
+ * The core skill list, skipping names that are not installed.
65
+ *
66
+ * A missing entry is not fatal: the point of the list is to cut the prompt
67
+ * tax, and a skill that was pruned or renamed upstream should quietly reduce
68
+ * the set rather than stop pi from starting.
69
+ */
70
+ function coreSkills() {
71
+ const file = path.join(root, 'core-skills.json');
72
+ let names;
73
+ try {
74
+ names = JSON.parse(readFileSync(file, 'utf8'));
75
+ } catch (err) {
76
+ console.error(`AOS CLI: core-skills.json unreadable (${err.message}) -- loading all skills.`);
77
+ return [];
78
+ }
79
+ if (!Array.isArray(names)) {
80
+ console.error('AOS CLI: core-skills.json is not a list -- loading all skills.');
81
+ return [];
82
+ }
83
+ const found = names.filter((n) => typeof n === 'string' && existsSync(path.join(agents, 'skills', n)));
84
+ const absent = names.length - found.length;
85
+ if (absent) console.error(`AOS CLI: ${absent} core skill(s) not installed, skipped.`);
86
+ return found;
87
+ }
88
+
89
+ // Find pi's CLI entry by walking up to the package root that declares it.
90
+ // Neither require.resolve nor a fixed depth works: pi's `exports` map exports
91
+ // only its library entry, so `@earendil-works/pi-coding-agent/package.json`
92
+ // and `/dist/bundle/cli.js` both throw ERR_PACKAGE_PATH_NOT_EXPORTED, and
93
+ // npm leaves no node_modules/.bin/pi behind for a transitive dependency.
94
+ // The resolved import specifier points inside the package, so from there the
95
+ // root is at most a couple of levels up.
96
+ function findPiEntry(startDir) {
97
+ let dir = startDir;
98
+ for (;;) {
99
+ const manifest = path.join(dir, 'package.json');
100
+ if (existsSync(manifest)) {
101
+ try {
102
+ const parsed = JSON.parse(readFileSync(manifest, 'utf8'));
103
+ if (parsed.bin && parsed.bin.pi) return path.resolve(dir, parsed.bin.pi);
104
+ } catch {
105
+ // unreadable or malformed -- keep walking, a parent's manifest may still be pi's
106
+ }
107
+ }
108
+ const parent = path.dirname(dir);
109
+ if (parent === dir) return null;
110
+ dir = parent;
111
+ }
112
+ }
113
+
114
+ function piInvocation() {
115
+ try {
116
+ const entry = findPiEntry(
117
+ path.dirname(fileURLToPath(import.meta.resolve('@earendil-works/pi-coding-agent')))
118
+ );
119
+ if (entry) return { command: process.execPath, prefix: [entry] };
120
+ } catch {
121
+ // pi is not installed next to us -- fall through to PATH
122
+ }
123
+ return { command: 'pi', prefix: [] };
124
+ }
125
+
126
+ const { command, prefix } = piInvocation();
127
+ const child = spawn(command, [...prefix, ...args, ...process.argv.slice(2)], { stdio: 'inherit' });
128
+
129
+ child.on('error', (err) => {
130
+ console.error(`AOS CLI: cannot start pi (${err.code ?? err.message}).`);
131
+ console.error(' Install it with: npm i -g @earendil-works/pi-coding-agent');
132
+ process.exit(1);
133
+ });
134
+ child.on('exit', (code, signal) => process.exit(signal ? 1 : code ?? 0));
@@ -0,0 +1,12 @@
1
+ [
2
+ "ask-tim",
3
+ "aos-setup",
4
+ "aos-project-init",
5
+ "mcp-manage",
6
+ "systematic-debugging",
7
+ "planning-with-files",
8
+ "token-saver-config",
9
+ "archify",
10
+ "deja-memory",
11
+ "grill-me"
12
+ ]
@@ -0,0 +1,321 @@
1
+ /**
2
+ * AOS CLI commands -- `/aos` and `/aos-status`.
3
+ *
4
+ * Read-only by default. The AOS installer owns the terminal: it animates, it
5
+ * prompts, and pi's own docs say an extension must not create a second
6
+ * terminal renderer. So anything that needs a real TTY (install, update,
7
+ * uninstall) is reported as a command to run outside the session rather than
8
+ * spawned here -- a nested installer inside pi's TUI corrupts both.
9
+ *
10
+ * `aos-doctor --json` is the only thing this executes, and it is read-only.
11
+ * The shape it returns is stable and is asserted by tests/aos-doctor.test.mjs:
12
+ * { ok, platform, arch, results: [{ area, name, ok, detail, warningOnly }] }
13
+ */
14
+
15
+ import { spawn } from "node:child_process";
16
+ import { connect } from "node:net";
17
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
18
+ // DynamicBorder is re-exported by the agent package, not by pi-tui -- the
19
+ // examples/extensions/preset.ts reference splits them the same way.
20
+ import { DynamicBorder } from "@earendil-works/pi-coding-agent";
21
+ import { Container, SelectItem, SelectList, Text } from "@earendil-works/pi-tui";
22
+
23
+ const DASHBOARD_PORT = 7900;
24
+ const DOCTOR_TIMEOUT_MS = 20_000;
25
+ const AUTH_TIMEOUT_MS = 8_000;
26
+
27
+ /**
28
+ * Providers to check for credentials, in priority order.
29
+ *
30
+ * A second provider slots in here and nowhere else -- the check, the status
31
+ * row and the startup notice all read this list. pi owns provider auth, not the
32
+ * AOS installer: it already has a `/login` command, and the AOS installer has no
33
+ * business asking for an Anthropic key it will never use itself. AOS CLI only
34
+ * has to notice when nothing is configured and point at `/login`.
35
+ *
36
+ * Deliberately short. Reporting "no configured provider (checked: ...)" with an
37
+ * honest list beats claiming a global state this process cannot actually see.
38
+ */
39
+ const PROVIDERS = ["anthropic", "google"] as const;
40
+
41
+ interface DoctorResult {
42
+ area: string;
43
+ name: string;
44
+ ok: boolean;
45
+ detail: string;
46
+ warningOnly: boolean;
47
+ }
48
+ interface DoctorReport {
49
+ ok: boolean;
50
+ results: DoctorResult[];
51
+ }
52
+
53
+ /**
54
+ * Run aos-doctor and return its report whatever the exit code.
55
+ *
56
+ * aos-doctor exits 1 whenever any single check is not ok -- that is the normal
57
+ * "something needs attention" answer, not a failure to run. Rejecting on a
58
+ * non-zero exit threw away a valid 27-check report and rendered as
59
+ * "aos-doctor could not be run", which is the worst possible reading: it
60
+ * reports the absence of a check as if the check had passed. The report's own
61
+ * `ok` field is the verdict; the exit code carries no extra information.
62
+ */
63
+ function runDoctor(): Promise<DoctorReport> {
64
+ return new Promise((resolve, reject) => {
65
+ // shell on Windows only: the global shims are .cmd there, and spawn
66
+ // does not resolve those without it.
67
+ const child = spawn("aos-doctor", ["--json"], {
68
+ timeout: DOCTOR_TIMEOUT_MS,
69
+ shell: process.platform === "win32",
70
+ });
71
+ let out = "";
72
+ let err = "";
73
+ child.stdout?.on("data", (c) => {
74
+ out += c;
75
+ });
76
+ child.stderr?.on("data", (c) => {
77
+ err += c;
78
+ });
79
+ child.once("error", reject);
80
+ child.once("close", (code) => {
81
+ try {
82
+ resolve(JSON.parse(out) as DoctorReport);
83
+ } catch {
84
+ reject(new Error(err.trim() || `aos-doctor produced no JSON (exit ${code})`));
85
+ }
86
+ });
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Ask pi whether one provider has usable credentials.
92
+ *
93
+ * `pi auth check --provider <name> --json` is a documented CLI surface and
94
+ * exits 0 for every verdict, including not_ready, so the JSON is the answer and
95
+ * the exit code carries nothing. `pi --list-models` was the tempting shortcut
96
+ * and is wrong: it lists every provider pi knows about whether or not a key is
97
+ * present, so a model in the list says nothing about being able to use it.
98
+ *
99
+ * A provider this check cannot reach at all is "unknown", never "not ready" --
100
+ * those are different states and reporting one as the other is how a health
101
+ * check ends up lying.
102
+ */
103
+ function checkProvider(provider: string): Promise<"ready" | "not_ready" | "unknown"> {
104
+ return new Promise((resolve) => {
105
+ // argv[1] is pi's own bundle, because pi is the process running this
106
+ // extension. PATH is the fallback for a launch that re-execs a bare `pi`.
107
+ const entry = process.argv[1];
108
+ const [command, prefix] =
109
+ entry && /pi/i.test(entry) ? [process.execPath, [entry]] : ["pi", []];
110
+
111
+ const child = spawn(command, [...prefix, "auth", "check", "--provider", provider, "--json"], {
112
+ timeout: AUTH_TIMEOUT_MS,
113
+ shell: process.platform === "win32",
114
+ });
115
+ let out = "";
116
+ child.stdout?.on("data", (c) => {
117
+ out += c;
118
+ });
119
+ child.once("error", () => resolve("unknown"));
120
+ child.once("close", () => {
121
+ try {
122
+ const parsed = JSON.parse(out) as { status?: string };
123
+ resolve(parsed.status === "ready" ? "ready" : "not_ready");
124
+ } catch {
125
+ resolve("unknown");
126
+ }
127
+ });
128
+ });
129
+ }
130
+
131
+ /** One row per provider, ready ones first, for the status overlay. */
132
+ async function providerStatus(): Promise<{ provider: string; state: string }[]> {
133
+ return Promise.all(
134
+ PROVIDERS.map(async (provider) => ({ provider, state: await checkProvider(provider) })),
135
+ );
136
+ }
137
+
138
+ function portOpen(port: number, timeoutMs = 500): Promise<boolean> {
139
+ return new Promise((resolve) => {
140
+ const socket = connect({ port, host: "127.0.0.1" });
141
+ const done = (up: boolean) => {
142
+ socket.destroy();
143
+ resolve(up);
144
+ };
145
+ socket.setTimeout(timeoutMs);
146
+ socket.once("connect", () => done(true));
147
+ socket.once("timeout", () => done(false));
148
+ socket.once("error", () => done(false));
149
+ });
150
+ }
151
+
152
+ async function showStatus(ctx: ExtensionContext): Promise<void> {
153
+ let report: DoctorReport | null = null;
154
+ let failure: string | null = null;
155
+ try {
156
+ report = await runDoctor();
157
+ } catch (err) {
158
+ failure = err instanceof Error ? err.message : String(err);
159
+ }
160
+ const dashboardUp = await portOpen(DASHBOARD_PORT);
161
+ const providers = await providerStatus();
162
+ const anyReady = providers.some((p) => p.state === "ready");
163
+
164
+ await ctx.ui.custom<void>((tui, theme, _kb, done) => {
165
+ const container = new Container();
166
+ container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
167
+
168
+ const mark = (ok: boolean) => theme.fg(ok ? "success" : "error", ok ? "●" : "○");
169
+
170
+ if (failure) {
171
+ container.addChild(new Text(theme.fg("error", "aos-doctor could not be run")));
172
+ container.addChild(new Text(theme.fg("muted", failure)));
173
+ container.addChild(new Text(theme.fg("dim", "It is installed by `npx @hybridlabor-api/aos`.")));
174
+ } else if (report) {
175
+ const failing = report.results.filter((r) => !r.ok);
176
+ container.addChild(
177
+ new Text(
178
+ theme.bold(
179
+ `AOS ${failing.length === 0 ? theme.fg("success", "healthy") : theme.fg("error", `${failing.length} failing check(s)`)}`,
180
+ ),
181
+ ),
182
+ );
183
+ container.addChild(new Text(""));
184
+ for (const r of report.results) {
185
+ container.addChild(new Text(`${mark(r.ok)} ${r.name}${r.ok ? "" : theme.fg("error", " ← needs attention")}`));
186
+ if (r.detail) container.addChild(new Text(theme.fg("muted", ` ${r.detail}`)));
187
+ }
188
+ }
189
+
190
+ container.addChild(new Text(""));
191
+ container.addChild(
192
+ new Text(
193
+ `${mark(dashboardUp)} Dashboard :${DASHBOARD_PORT} ${
194
+ dashboardUp ? theme.fg("success", "running") : theme.fg("dim", "stopped -- run `npx aos-dashboard`")
195
+ }`,
196
+ ),
197
+ );
198
+ container.addChild(
199
+ new Text(
200
+ `${mark(anyReady)} Model ${
201
+ anyReady
202
+ ? theme.fg("success", providers.filter((p) => p.state === "ready").map((p) => p.provider).join(", "))
203
+ : theme.fg("error", "no provider configured -- run /login in pi")
204
+ }`,
205
+ ),
206
+ );
207
+ container.addChild(new Text(""));
208
+ container.addChild(new Text(theme.fg("dim", "esc or enter to close")));
209
+ container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
210
+
211
+ return {
212
+ render(width: number) {
213
+ return container.render(width);
214
+ },
215
+ invalidate() {
216
+ container.invalidate();
217
+ },
218
+ handleInput(data: string) {
219
+ if (data === "" || data === "\r" || data === "\n" || data === "") done();
220
+ tui.requestRender();
221
+ },
222
+ };
223
+ });
224
+ }
225
+
226
+ export default function aosCommands(pi: ExtensionAPI) {
227
+ // Fire and forget on session_start: never block startup on it, and never
228
+ // repeat. A user without credentials can still read skills, run
229
+ // /aos-status and browse -- only sending a message needs a model, so this is
230
+ // a notice, not a gate.
231
+ //
232
+ // It says "at startup" on purpose. This handler runs once, so a /login
233
+ // completed inside the session leaves the warning standing next to a working
234
+ // model -- and a notice that is visible after you acted on it is worse than
235
+ // none, because it reads as a current fact. pi has no event this extension
236
+ // can rely on for "a login just landed", so rather than invent a poll or
237
+ // guess at an API, the wording states when it was true and /aos-status is
238
+ // the live reading. Measured: `pi auth check` returns ready/oauth once the
239
+ // browser flow completes, so that command and nothing else is the truth.
240
+ let warnedNoProvider = false;
241
+ pi.on("session_start", async (_event, ctx) => {
242
+ if (warnedNoProvider) return;
243
+ const providers = await providerStatus();
244
+ if (providers.some((p) => p.state === "ready")) return;
245
+ warnedNoProvider = true;
246
+ const unknown = providers.filter((p) => p.state === "unknown").map((p) => p.provider);
247
+ ctx.ui.notify(
248
+ `No model provider was configured at startup (checked: ${providers.map((p) => p.provider).join(", ")}). ` +
249
+ `Run /login to add one -- or /aos-status for the current state.`,
250
+ "warning",
251
+ );
252
+ if (unknown.length) {
253
+ ctx.ui.notify(`Could not reach pi's auth check for: ${unknown.join(", ")} -- treated as unverified, not as unconfigured.`, "info");
254
+ }
255
+ });
256
+
257
+ pi.registerCommand("aos-status", {
258
+ description: "AOS health, failing checks and daemon status",
259
+ handler: async (_args, ctx) => {
260
+ await showStatus(ctx);
261
+ },
262
+ });
263
+
264
+ pi.registerCommand("aos", {
265
+ description: "AOS menu: status, dashboard, or the command for a terminal action",
266
+ handler: async (_args, ctx) => {
267
+ const items: SelectItem[] = [
268
+ { value: "status", label: "Status", description: "run aos-doctor and show failing checks" },
269
+ { value: "update", label: "Skills & MCPs aktualisieren", description: "runs outside this session" },
270
+ { value: "install", label: "AOS CLI nachinstallieren", description: "runs outside this session" },
271
+ { value: "uninstall", label: "AOS deinstallieren", description: "runs outside this session" },
272
+ ];
273
+
274
+ const choice = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
275
+ const container = new Container();
276
+ container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
277
+ container.addChild(new Text(theme.fg("accent", theme.bold("AOS"))));
278
+
279
+ const list = new SelectList(items, items.length, {
280
+ selectedPrefix: (t) => theme.fg("accent", t),
281
+ selectedText: (t) => theme.fg("accent", t),
282
+ description: (t) => theme.fg("muted", t),
283
+ });
284
+ list.onSelect = (item) => done(item.value);
285
+ list.onCancel = () => done(null);
286
+ container.addChild(list);
287
+
288
+ container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel")));
289
+ container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
290
+
291
+ return {
292
+ render(width: number) {
293
+ return container.render(width);
294
+ },
295
+ invalidate() {
296
+ container.invalidate();
297
+ },
298
+ handleInput(data: string) {
299
+ list.handleInput(data);
300
+ tui.requestRender();
301
+ },
302
+ };
303
+ });
304
+
305
+ if (choice === "status") {
306
+ await showStatus(ctx);
307
+ return;
308
+ }
309
+ if (!choice) return;
310
+
311
+ // The installer animates and prompts; running it with inherited stdio
312
+ // inside pi's TUI corrupts both renderers. Report the command instead.
313
+ const commands: Record<string, string> = {
314
+ update: "aos",
315
+ install: "aos",
316
+ uninstall: "npx @hybridlabor-api/aos-uninstall",
317
+ };
318
+ ctx.ui.notify(`Run this in a separate terminal: ${commands[choice]}`, "info");
319
+ },
320
+ });
321
+ }