otacon 0.1.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.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/dist/cli/client.js +188 -0
- package/dist/cli/client.js.map +1 -0
- package/dist/cli/commands/answer.js +63 -0
- package/dist/cli/commands/answer.js.map +1 -0
- package/dist/cli/commands/ask.js +117 -0
- package/dist/cli/commands/ask.js.map +1 -0
- package/dist/cli/commands/clean.js +48 -0
- package/dist/cli/commands/clean.js.map +1 -0
- package/dist/cli/commands/doctor.js +86 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/expose.js +104 -0
- package/dist/cli/commands/expose.js.map +1 -0
- package/dist/cli/commands/implement-done.js +53 -0
- package/dist/cli/commands/implement-done.js.map +1 -0
- package/dist/cli/commands/install.js +113 -0
- package/dist/cli/commands/install.js.map +1 -0
- package/dist/cli/commands/open.js +37 -0
- package/dist/cli/commands/open.js.map +1 -0
- package/dist/cli/commands/progress.js +45 -0
- package/dist/cli/commands/progress.js.map +1 -0
- package/dist/cli/commands/start.js +66 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/status.js +44 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/submit.js +64 -0
- package/dist/cli/commands/submit.js.map +1 -0
- package/dist/cli/commands/wait.js +66 -0
- package/dist/cli/commands/wait.js.map +1 -0
- package/dist/cli/install/assets.js +285 -0
- package/dist/cli/install/assets.js.map +1 -0
- package/dist/cli/install/locations.js +92 -0
- package/dist/cli/install/locations.js.map +1 -0
- package/dist/cli/install/tailscale.js +39 -0
- package/dist/cli/install/tailscale.js.map +1 -0
- package/dist/cli/main.js +73 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/output.js +39 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/session.js +77 -0
- package/dist/cli/session.js.map +1 -0
- package/dist/daemon/activity.js +56 -0
- package/dist/daemon/activity.js.map +1 -0
- package/dist/daemon/anchor.js +143 -0
- package/dist/daemon/anchor.js.map +1 -0
- package/dist/daemon/app.js +1081 -0
- package/dist/daemon/app.js.map +1 -0
- package/dist/daemon/approve.js +71 -0
- package/dist/daemon/approve.js.map +1 -0
- package/dist/daemon/desktop-notify.js +69 -0
- package/dist/daemon/desktop-notify.js.map +1 -0
- package/dist/daemon/diff.js +187 -0
- package/dist/daemon/diff.js.map +1 -0
- package/dist/daemon/linter/index.js +19 -0
- package/dist/daemon/linter/index.js.map +1 -0
- package/dist/daemon/linter/parse.js +350 -0
- package/dist/daemon/linter/parse.js.map +1 -0
- package/dist/daemon/linter/rules.js +359 -0
- package/dist/daemon/linter/rules.js.map +1 -0
- package/dist/daemon/main.js +48 -0
- package/dist/daemon/main.js.map +1 -0
- package/dist/daemon/notify.js +23 -0
- package/dist/daemon/notify.js.map +1 -0
- package/dist/daemon/presence.js +37 -0
- package/dist/daemon/presence.js.map +1 -0
- package/dist/daemon/queue.js +160 -0
- package/dist/daemon/queue.js.map +1 -0
- package/dist/daemon/store.js +393 -0
- package/dist/daemon/store.js.map +1 -0
- package/dist/daemon/threads.js +153 -0
- package/dist/daemon/threads.js.map +1 -0
- package/dist/daemon/transcript.js +89 -0
- package/dist/daemon/transcript.js.map +1 -0
- package/dist/daemon/ui.js +175 -0
- package/dist/daemon/ui.js.map +1 -0
- package/dist/shared/config.js +93 -0
- package/dist/shared/config.js.map +1 -0
- package/dist/shared/gwt.js +69 -0
- package/dist/shared/gwt.js.map +1 -0
- package/dist/shared/paths.js +67 -0
- package/dist/shared/paths.js.map +1 -0
- package/dist/shared/question-spec.js +44 -0
- package/dist/shared/question-spec.js.map +1 -0
- package/dist/shared/types.js +35 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/shared/version.js +5 -0
- package/dist/shared/version.js.map +1 -0
- package/dist/ui/assets/arc-HhPfdCPZ.js +1 -0
- package/dist/ui/assets/architecture-7EHR7CIX-BPLblcyi.js +1 -0
- package/dist/ui/assets/architectureDiagram-3BPJPVTR-D2PIxGOb.js +36 -0
- package/dist/ui/assets/array-BifhSqXX.js +1 -0
- package/dist/ui/assets/blockDiagram-GPEHLZMM-DQ3Dn17h.js +132 -0
- package/dist/ui/assets/c4Diagram-AAUBKEIU-DxITrQgS.js +10 -0
- package/dist/ui/assets/channel-ipcU8ZNI.js +1 -0
- package/dist/ui/assets/chunk-2J33WTMH-Du1JoPx5.js +1 -0
- package/dist/ui/assets/chunk-3OPIFGDE-Dn7x2Yqf.js +62 -0
- package/dist/ui/assets/chunk-4BX2VUAB-DVnrE-4n.js +1 -0
- package/dist/ui/assets/chunk-55IACEB6-BAhFAimA.js +1 -0
- package/dist/ui/assets/chunk-5ZQYHXKU-0hEZptem.js +2 -0
- package/dist/ui/assets/chunk-727SXJPM-C1FN_cI3.js +206 -0
- package/dist/ui/assets/chunk-AQP2D5EJ-A656OBd4.js +231 -0
- package/dist/ui/assets/chunk-BSJP7CBP-D8oMbjm8.js +1 -0
- package/dist/ui/assets/chunk-CSCIHK7Q-DjIL8GLi.js +122 -0
- package/dist/ui/assets/chunk-FMBD7UC4-Otblfqvz.js +15 -0
- package/dist/ui/assets/chunk-KSCS5N6A-BOjTvm3H.js +10 -0
- package/dist/ui/assets/chunk-L5ZTLDWV-CaTLaw6L.js +1 -0
- package/dist/ui/assets/chunk-LZXEDZCA-Dq5p7qrD.js +2 -0
- package/dist/ui/assets/chunk-ND2GUHAM-jZ_NNnWi.js +1 -0
- package/dist/ui/assets/chunk-NNHCCRGN-DlpIbxXb.js +159 -0
- package/dist/ui/assets/chunk-NZK2D7GU-U_7l_sCh.js +1 -0
- package/dist/ui/assets/chunk-O5CBEL6O-MewqqNB7.js +70 -0
- package/dist/ui/assets/chunk-QZHKN3VN-DzGPH44B.js +1 -0
- package/dist/ui/assets/chunk-WU5MYG2G-DyEIVjoo.js +1 -0
- package/dist/ui/assets/chunk-XPW4576I-D5ArxNEF.js +32 -0
- package/dist/ui/assets/classDiagram-4FO5ZUOK-Byg2Hl9D.js +1 -0
- package/dist/ui/assets/classDiagram-v2-Q7XG4LA2-Byg2Hl9D.js +1 -0
- package/dist/ui/assets/cose-bilkent-S5V4N54A-PFXzf7WV.js +1 -0
- package/dist/ui/assets/cytoscape.esm-h6BdjjI9.js +321 -0
- package/dist/ui/assets/dagre-BM42HDAG-xrCfjZuZ.js +4 -0
- package/dist/ui/assets/dagre-Bx709z4p.js +1 -0
- package/dist/ui/assets/defaultLocale-C8Fc0cco.js +1 -0
- package/dist/ui/assets/diagram-2AECGRRQ-BFf-cyKY.js +43 -0
- package/dist/ui/assets/diagram-5GNKFQAL-kNPV4NfV.js +10 -0
- package/dist/ui/assets/diagram-KO2AKTUF-ByC1IUwG.js +3 -0
- package/dist/ui/assets/diagram-LMA3HP47-DZIJMPK0.js +24 -0
- package/dist/ui/assets/diagram-OG6HWLK6-CSDED9A-.js +24 -0
- package/dist/ui/assets/dist-YwjsDswi.js +1 -0
- package/dist/ui/assets/erDiagram-TEJ5UH35-yuzvjE6J.js +85 -0
- package/dist/ui/assets/eventmodeling-FCH6USID-CZR4eNG-.js +1 -0
- package/dist/ui/assets/flowDiagram-I6XJVG4X-ApPtVyYM.js +162 -0
- package/dist/ui/assets/ganttDiagram-6RSMTGT7-BeMLXtAr.js +292 -0
- package/dist/ui/assets/gitGraph-WXDBUCRP-JmTTBa7j.js +1 -0
- package/dist/ui/assets/gitGraphDiagram-PVQCEYII-Cjjnjs71.js +106 -0
- package/dist/ui/assets/graphlib-B8gBHxth.js +1 -0
- package/dist/ui/assets/index-BFQVRcSI.js +11 -0
- package/dist/ui/assets/index-Bj_kTrwP.css +1 -0
- package/dist/ui/assets/info-J43DQDTF-8vZ3gome.js +1 -0
- package/dist/ui/assets/infoDiagram-5YYISTIA-CnMk1cA-.js +2 -0
- package/dist/ui/assets/init-D6jRqBbL.js +1 -0
- package/dist/ui/assets/ishikawaDiagram-YF4QCWOH-Bl8z6huD.js +70 -0
- package/dist/ui/assets/journeyDiagram-JHISSGLW-DYIVfMpS.js +139 -0
- package/dist/ui/assets/kanban-definition-UN3LZRKU-BnR0ZzOz.js +89 -0
- package/dist/ui/assets/katex-Vhh-h91d.js +257 -0
- package/dist/ui/assets/line-DcBdQit6.js +1 -0
- package/dist/ui/assets/linear-HKjRHFAO.js +1 -0
- package/dist/ui/assets/mermaid-parser.core-DkYXrPlA.js +4 -0
- package/dist/ui/assets/mermaid.core-BmkfCI3b.js +9 -0
- package/dist/ui/assets/mindmap-definition-RKZ34NQL-sIAd4nDi.js +96 -0
- package/dist/ui/assets/ordinal-hYBb2elL.js +1 -0
- package/dist/ui/assets/otacon-DPXGiaVj.svg +11 -0
- package/dist/ui/assets/packet-YPE3B663-BxbxcfXN.js +1 -0
- package/dist/ui/assets/path-BWPyau1x.js +1 -0
- package/dist/ui/assets/pie-LRSECV5Y-BJxazjNs.js +1 -0
- package/dist/ui/assets/pieDiagram-4H26LBE5-BiOhc9GR.js +30 -0
- package/dist/ui/assets/plan-view-CH6NzUDb.js +74 -0
- package/dist/ui/assets/purify.es-CDvCXckx.js +3 -0
- package/dist/ui/assets/quadrantDiagram-W4KKPZXB-CVyHbWgo.js +7 -0
- package/dist/ui/assets/radar-GUYGQ44K-D9ohbnbV.js +1 -0
- package/dist/ui/assets/requirementDiagram-4Y6WPE33-Ba24_hqc.js +84 -0
- package/dist/ui/assets/rough.esm-CSKSodPl.js +1 -0
- package/dist/ui/assets/sankeyDiagram-5OEKKPKP-CxD4wiPL.js +40 -0
- package/dist/ui/assets/sequenceDiagram-3UESZ5HK-7qA7lD61.js +162 -0
- package/dist/ui/assets/src-IM8AE8MK.js +1 -0
- package/dist/ui/assets/stateDiagram-AJRCARHV-DNElRCuH.js +1 -0
- package/dist/ui/assets/stateDiagram-v2-BHNVJYJU-D6qTYpY3.js +1 -0
- package/dist/ui/assets/timeline-definition-PNZ67QCA-ChYC4Grd.js +120 -0
- package/dist/ui/assets/treeView-BLDUP644-Il0KnMi_.js +1 -0
- package/dist/ui/assets/treemap-LRROVOQU-CIiKcdRo.js +1 -0
- package/dist/ui/assets/vennDiagram-CIIHVFJN-Ulhkum9i.js +34 -0
- package/dist/ui/assets/wardley-L42UT6IY-BNd4ljz7.js +1 -0
- package/dist/ui/assets/wardleyDiagram-YWT4CUSO-BicXxh84.js +78 -0
- package/dist/ui/assets/xychartDiagram-2RQKCTM6-Duf-m_th.js +7 -0
- package/dist/ui/index.html +20 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zero Liu
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<img width="9034" height="1857" alt="github-otacon" src="https://github.com/user-attachments/assets/18796111-63d7-4df6-a2ba-b95f132eabd3" />
|
|
2
|
+
|
|
3
|
+
Plan review surface for coding agents (Claude Code, Codex, OpenCode). Replaces native
|
|
4
|
+
plan modes with one CLI protocol: schema'd concise plans, anchored comments and
|
|
5
|
+
questions from any device (phone included, over Tailscale), revision diffs against
|
|
6
|
+
what you last reviewed, and a mandatory grill-me interview phase before any plan
|
|
7
|
+
reaches review. Approval produces a committed plan artifact for a future implementer
|
|
8
|
+
skill (`snake`) to execute.
|
|
9
|
+
|
|
10
|
+
Behavior spec: [DESIGN.md](DESIGN.md) · tradeoff rationale: [DECISIONS.md](DECISIONS.md)
|
|
11
|
+
· agent conventions: [AGENTS.md](AGENTS.md)
|
|
12
|
+
|
|
13
|
+
Personal tool by/for Zero. Zero API spend by construction — all model work happens in
|
|
14
|
+
your interactive subscription-backed agent session; the daemon, CLI, and UI never call
|
|
15
|
+
an LLM.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
One-time machine setup (DESIGN.md §16):
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install -g otacon # one package: CLI + daemon (Node ≥ 20)
|
|
23
|
+
otacon install --all # agent wrappers; or --agent claude|codex|opencode
|
|
24
|
+
otacon install --agent claude --hooks # also register the Claude Code Stop hook
|
|
25
|
+
otacon doctor # verify: node, daemon boots, wrappers, Tailscale
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`otacon install` writes the protocol wrapper into each agent's skill location —
|
|
29
|
+
Claude Code: `~/.claude/skills/otacon/SKILL.md` (+ the Stop hook script at
|
|
30
|
+
`~/.claude/hooks/otacon-stop.sh`); Codex: a marked block in `~/.codex/AGENTS.md`;
|
|
31
|
+
OpenCode: `~/.config/opencode/skills/otacon/SKILL.md`. Wrappers are managed files:
|
|
32
|
+
reinstalls overwrite them (outside-the-markers content in Codex's shared file
|
|
33
|
+
survives). `--hooks` merges the Stop hook into `~/.claude/settings.json` additively
|
|
34
|
+
and idempotently, backing the file up first. The daemon is never started by hand —
|
|
35
|
+
any `otacon` command auto-spawns it.
|
|
36
|
+
|
|
37
|
+
Per-repo setup: **none.** The first `otacon start` in a repo creates `.otacon/` and
|
|
38
|
+
gitignores it. Approved plans land committed in `docs/plans/`. `otacon clean` archives
|
|
39
|
+
ended sessions' working state to `.otacon/archive/`.
|
|
40
|
+
|
|
41
|
+
### Updating
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npm update -g otacon # the version handshake restarts the daemon on next use
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Build from source (contributors)
|
|
48
|
+
|
|
49
|
+
For contributors or the bleeding edge only — the published npm package is the
|
|
50
|
+
supported user path. Clone the repo and run from source:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
git clone https://github.com/zeroliu/otacon && cd otacon
|
|
54
|
+
bun install
|
|
55
|
+
./bin/otacon doctor # run straight from source
|
|
56
|
+
# — or build a Node artifact and link it onto PATH —
|
|
57
|
+
bun run build && npm link # `otacon` now points at this checkout
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
(`npm i -g github:zeroliu/otacon` is **not** a supported install — the published
|
|
61
|
+
package ships a prebuilt `dist/`, and a GitHub install would need a build-on-install
|
|
62
|
+
step that is intentionally not wired.)
|
|
63
|
+
|
|
64
|
+
## Phone access
|
|
65
|
+
|
|
66
|
+
Reviews work from a phone over Tailscale (DESIGN.md §11) — plans never leave your
|
|
67
|
+
devices, and the tailnet is the auth:
|
|
68
|
+
|
|
69
|
+
1. Install Tailscale on the Mac and the phone; log in (`tailscale up`).
|
|
70
|
+
2. Enable **HTTPS Certificates** for the tailnet: Tailscale admin console → DNS →
|
|
71
|
+
Enable HTTPS (MagicDNS must be on). This is the one step otacon cannot do for you.
|
|
72
|
+
3. `otacon expose` — configures `tailscale serve` for the daemon port, verifies the
|
|
73
|
+
tailnet URL actually serves, and prints the HTTPS URL with `verified: true`.
|
|
74
|
+
Bookmark it on the phone.
|
|
75
|
+
4. Keep the Mac awake while a plan is in review: `caffeinate -i`.
|
|
76
|
+
|
|
77
|
+
If you skip step 2, `tailscale serve` still succeeds but the URL resets every TLS
|
|
78
|
+
handshake — so `otacon expose` reports `verified: false` and links the admin DNS page
|
|
79
|
+
instead of handing you a dead URL. (Just enabled HTTPS? The cert can take a minute to
|
|
80
|
+
provision; re-run `expose`.)
|
|
81
|
+
|
|
82
|
+
On the Mac App Store Tailscale, putting `tailscale` on your `PATH` needs a manual
|
|
83
|
+
launcher — a wrapper script that runs the app-bundle binary (a bare symlink crashes).
|
|
84
|
+
otacon finds the app-bundle binary on its own either way.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
Maintainers cutting a release: see [RELEASING.md](RELEASING.md).
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// HTTP client for otacond, plus ensureDaemon — the auto-spawn and version
|
|
2
|
+
// handshake every CLI command runs first (DESIGN.md §16).
|
|
3
|
+
//
|
|
4
|
+
// The daemon is spawned by resolved path — the dist/daemon/main.js sibling of
|
|
5
|
+
// this very module — never via PATH (DECISIONS.md "Daemon spawned by resolved
|
|
6
|
+
// file path"), detached with stdout/stderr appended to $OTACON_HOME/daemon.log.
|
|
7
|
+
// Instead of reading the boot line, the CLI re-probes /api/health and watches
|
|
8
|
+
// the child's exit code: exit 0 before health means it lost the spawn race to
|
|
9
|
+
// another otacond (the port is the lock — keep polling the winner); any other
|
|
10
|
+
// exit is a refusal or crash, surfaced with a pointer at the log (DECISIONS.md
|
|
11
|
+
// "Daemon spawn: health re-probe, not the boot line").
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { closeSync, existsSync, mkdirSync, openSync } from "node:fs";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { daemonLogPath, otaconHome, otaconPort } from "../shared/paths.js";
|
|
16
|
+
import { VERSION } from "../shared/version.js";
|
|
17
|
+
import { fail, notice } from "./output.js";
|
|
18
|
+
const PROBE_TIMEOUT_MS = 1500;
|
|
19
|
+
const SPAWN_DEADLINE_MS = 8000;
|
|
20
|
+
const POLL_INTERVAL_MS = 100;
|
|
21
|
+
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
export function baseUrl() {
|
|
23
|
+
return `http://127.0.0.1:${otaconPort()}`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* JSON request/response against the daemon. A connection failure (refused,
|
|
27
|
+
* reset, aborted, truncated body) throws E_DAEMON_DOWN — an actionable exit-1
|
|
28
|
+
* failure per the exit-code contract, not an internal error. `otacon wait`
|
|
29
|
+
* treats exactly that code as "back off and re-park".
|
|
30
|
+
*/
|
|
31
|
+
export async function api(method, path, body, signal) {
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(`${baseUrl()}${path}`, {
|
|
34
|
+
method,
|
|
35
|
+
headers: body === undefined ? undefined : { "content-type": "application/json" },
|
|
36
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
37
|
+
signal,
|
|
38
|
+
});
|
|
39
|
+
// Every /api response is JSON; a body that fails to parse is a connection
|
|
40
|
+
// truncated mid-response and must NOT pass as an empty result — for events
|
|
41
|
+
// that would print {} as the event while the daemon requeues it.
|
|
42
|
+
const parsed = (await response.json());
|
|
43
|
+
return { status: response.status, body: parsed };
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
47
|
+
fail("E_DAEMON_DOWN", `cannot reach otacond at ${baseUrl()}${path}: ${message}; retry`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** down = nothing answered HTTP; foreign = answered but is not otacond. */
|
|
51
|
+
async function probe() {
|
|
52
|
+
let response;
|
|
53
|
+
try {
|
|
54
|
+
response = await fetch(`${baseUrl()}/api/health`, {
|
|
55
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { state: "down" };
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const health = (await response.json());
|
|
63
|
+
if (health.app === "otacond" && typeof health.version === "string") {
|
|
64
|
+
return { state: "up", health };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// non-JSON body: not otacond
|
|
69
|
+
}
|
|
70
|
+
return { state: "foreign" };
|
|
71
|
+
}
|
|
72
|
+
function portConflict() {
|
|
73
|
+
fail("E_PORT_CONFLICT", `port ${otaconPort()} is in use by something that is not otacond; set OTACON_PORT to pick another port`);
|
|
74
|
+
}
|
|
75
|
+
/** dist/daemon/main.js sibling; from a source-tree run (bun) the .ts entry. */
|
|
76
|
+
function daemonEntry() {
|
|
77
|
+
const built = fileURLToPath(new URL("../daemon/main.js", import.meta.url));
|
|
78
|
+
if (existsSync(built))
|
|
79
|
+
return built;
|
|
80
|
+
// Running from src/ (e.g. `bun run src/cli/main.ts`): no built sibling, but
|
|
81
|
+
// process.execPath is bun, which runs the TypeScript entry directly — this
|
|
82
|
+
// is what keeps "local dev behaves identically" true (DECISIONS.md "Daemon
|
|
83
|
+
// spawned by resolved file path").
|
|
84
|
+
return fileURLToPath(new URL("../daemon/main.ts", import.meta.url));
|
|
85
|
+
}
|
|
86
|
+
/** Spawn the daemon detached; returns the child's exit code, undefined while alive. */
|
|
87
|
+
function spawnDaemon() {
|
|
88
|
+
mkdirSync(otaconHome(), { recursive: true });
|
|
89
|
+
const log = openSync(daemonLogPath(), "a");
|
|
90
|
+
const child = spawn(process.execPath, [daemonEntry()], {
|
|
91
|
+
detached: true,
|
|
92
|
+
stdio: ["ignore", log, log],
|
|
93
|
+
});
|
|
94
|
+
child.unref();
|
|
95
|
+
closeSync(log); // the child holds its own descriptor
|
|
96
|
+
let exitCode;
|
|
97
|
+
child.on("error", (error) => {
|
|
98
|
+
// spawn itself failed (ENOENT/EACCES); without a listener this event
|
|
99
|
+
// would crash the CLI outside the JSON-on-stdout contract.
|
|
100
|
+
notice(`failed to spawn otacond: ${error.message}`);
|
|
101
|
+
exitCode = 1;
|
|
102
|
+
});
|
|
103
|
+
child.on("exit", (code) => {
|
|
104
|
+
exitCode = code ?? 1; // signal death counts as failure
|
|
105
|
+
});
|
|
106
|
+
return () => exitCode;
|
|
107
|
+
}
|
|
108
|
+
async function spawnAndAwaitHealth() {
|
|
109
|
+
const exitCode = spawnDaemon();
|
|
110
|
+
const deadline = Date.now() + SPAWN_DEADLINE_MS;
|
|
111
|
+
while (Date.now() < deadline) {
|
|
112
|
+
const result = await probe();
|
|
113
|
+
if (result.state === "up")
|
|
114
|
+
return result.health;
|
|
115
|
+
if (result.state === "foreign")
|
|
116
|
+
portConflict();
|
|
117
|
+
const code = exitCode();
|
|
118
|
+
if (code !== undefined && code !== 0) {
|
|
119
|
+
// Covers the non-HTTP port squatter too: the probe sees "down", the
|
|
120
|
+
// spawned daemon hits EADDRINUSE, fails its own ownership check, exits 1.
|
|
121
|
+
fail("E_DAEMON_START", `otacond exited with code ${code} before becoming healthy; see ${daemonLogPath()} (if the port is taken, set OTACON_PORT)`);
|
|
122
|
+
}
|
|
123
|
+
await sleep(POLL_INTERVAL_MS);
|
|
124
|
+
}
|
|
125
|
+
fail("E_DAEMON_START", `otacond did not become healthy on ${baseUrl()}; see ${daemonLogPath()}`);
|
|
126
|
+
}
|
|
127
|
+
async function shutdownStaleDaemon() {
|
|
128
|
+
// Re-check identity and version immediately before firing the shutdown: the
|
|
129
|
+
// caller probed moments ago, but a peer CLI may have completed the same
|
|
130
|
+
// restart in between — killing the healthy current-version daemon it just
|
|
131
|
+
// spawned would drop parked waiters for nothing (the probe→shutdown TOCTOU).
|
|
132
|
+
const recheck = await probe();
|
|
133
|
+
if (recheck.state === "down")
|
|
134
|
+
return;
|
|
135
|
+
if (recheck.state === "foreign")
|
|
136
|
+
portConflict();
|
|
137
|
+
if (recheck.health.version === VERSION)
|
|
138
|
+
return;
|
|
139
|
+
try {
|
|
140
|
+
await api("POST", "/api/shutdown");
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// it may drop the connection while exiting; the down-poll below decides
|
|
144
|
+
}
|
|
145
|
+
const deadline = Date.now() + SPAWN_DEADLINE_MS;
|
|
146
|
+
while (Date.now() < deadline) {
|
|
147
|
+
const result = await probe();
|
|
148
|
+
if (result.state === "down")
|
|
149
|
+
return;
|
|
150
|
+
// A concurrent CLI may have already respawned the current version into
|
|
151
|
+
// the gap; that restart is as good as ours — without this check we would
|
|
152
|
+
// poll a healthy daemon for the full deadline and fail spuriously.
|
|
153
|
+
if (result.state === "up" && result.health.version === VERSION)
|
|
154
|
+
return;
|
|
155
|
+
await sleep(POLL_INTERVAL_MS);
|
|
156
|
+
}
|
|
157
|
+
fail("E_DAEMON_RESTART", "stale otacond did not exit after POST /api/shutdown");
|
|
158
|
+
}
|
|
159
|
+
/** Whole probe→shutdown→respawn cycles before giving up on a version fight. */
|
|
160
|
+
const RESTART_ATTEMPTS = 3;
|
|
161
|
+
/**
|
|
162
|
+
* Health probe → spawn if down → exact-version handshake, restarting a stale
|
|
163
|
+
* daemon via POST /api/shutdown (DESIGN.md §16). Refuses a port held by a
|
|
164
|
+
* non-otacond process. The restart cycle is bounded (peer CLIs of different
|
|
165
|
+
* versions could otherwise ping-pong restarts forever), and the shutdown
|
|
166
|
+
* re-checks the daemon's version right before firing, so a peer's completed
|
|
167
|
+
* restart is adopted instead of killed (DECISIONS.md "Stale-daemon restart").
|
|
168
|
+
*/
|
|
169
|
+
export async function ensureDaemon() {
|
|
170
|
+
for (let attempt = 0; attempt < RESTART_ATTEMPTS; attempt++) {
|
|
171
|
+
const current = await probe();
|
|
172
|
+
if (current.state === "foreign")
|
|
173
|
+
portConflict();
|
|
174
|
+
if (current.state === "up") {
|
|
175
|
+
if (current.health.version === VERSION)
|
|
176
|
+
return current.health;
|
|
177
|
+
notice(`restarting stale otacond ${current.health.version} → ${VERSION}`);
|
|
178
|
+
await shutdownStaleDaemon();
|
|
179
|
+
continue; // re-probe: a peer may have respawned the current version
|
|
180
|
+
}
|
|
181
|
+
const health = await spawnAndAwaitHealth();
|
|
182
|
+
if (health.version === VERSION)
|
|
183
|
+
return health;
|
|
184
|
+
// An older CLI won the respawn race; loop to shut its daemon down too.
|
|
185
|
+
}
|
|
186
|
+
fail("E_VERSION_MISMATCH", `daemon on port ${otaconPort()} still runs another version after ${RESTART_ATTEMPTS} restart attempts — a CLI of a different version keeps respawning it; retry`);
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/cli/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// otacon answer <question-id> (--body "…" | --file f.md) [--session id] — the
|
|
2
|
+
// agent's reply to a user question (DESIGN.md §6, §9): the answer lands on the
|
|
3
|
+
// question's thread, the plan and status stay untouched, and the agent goes
|
|
4
|
+
// back to `otacon wait`.
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
8
|
+
import { api, ensureDaemon } from "../client.js";
|
|
9
|
+
import { fail, printJson, usageError } from "../output.js";
|
|
10
|
+
import { listSessions, realpathOr, resolveSession } from "../session.js";
|
|
11
|
+
export async function answerCommand(argv) {
|
|
12
|
+
const { values, positionals } = parseArgs({
|
|
13
|
+
args: argv,
|
|
14
|
+
options: {
|
|
15
|
+
body: { type: "string" },
|
|
16
|
+
file: { type: "string" },
|
|
17
|
+
session: { type: "string" },
|
|
18
|
+
},
|
|
19
|
+
allowPositionals: true,
|
|
20
|
+
});
|
|
21
|
+
const questionId = positionals[0];
|
|
22
|
+
if (questionId === undefined || positionals.length > 1) {
|
|
23
|
+
usageError("otacon answer takes exactly one <question-id>");
|
|
24
|
+
}
|
|
25
|
+
if ((values.body === undefined) === (values.file === undefined)) {
|
|
26
|
+
usageError("otacon answer requires exactly one of --body or --file");
|
|
27
|
+
}
|
|
28
|
+
let body;
|
|
29
|
+
if (values.body !== undefined) {
|
|
30
|
+
body = values.body;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const path = resolve(values.file);
|
|
34
|
+
try {
|
|
35
|
+
body = readFileSync(path, "utf8");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
fail("E_NO_FILE", `cannot read answer file ${path}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (body.trim() === "")
|
|
42
|
+
usageError("the answer body must be non-empty");
|
|
43
|
+
await ensureDaemon();
|
|
44
|
+
const session = resolveSession(await listSessions(), values.session, realpathOr(process.cwd()));
|
|
45
|
+
const response = await api("POST", `/api/sessions/${session.id}/questions/${encodeURIComponent(questionId)}/answer`, { body });
|
|
46
|
+
if (response.status === 200) {
|
|
47
|
+
printJson(response.body);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
const code = response.body.error?.code;
|
|
51
|
+
if (response.status === 404 && code === "E_UNKNOWN_QUESTION") {
|
|
52
|
+
fail("E_UNKNOWN_QUESTION", `session ${session.id} has no question ${questionId}`);
|
|
53
|
+
}
|
|
54
|
+
if (response.status === 404) {
|
|
55
|
+
fail("E_UNKNOWN_SESSION", `daemon no longer knows session ${session.id}`);
|
|
56
|
+
}
|
|
57
|
+
if (response.status === 409) {
|
|
58
|
+
const message = response.body?.error?.message;
|
|
59
|
+
fail("E_SESSION_OVER", message ?? `session ${session.id} is approved — the session is over`);
|
|
60
|
+
}
|
|
61
|
+
fail("E_INTERNAL", `answer failed: ${JSON.stringify(response.body)}`, undefined, 2);
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=answer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"answer.js","sourceRoot":"","sources":["../../../src/cli/commands/answer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// otacon ask --question "…" [--options "A|B|C"] [--recommend A] [--multi]
|
|
2
|
+
// [--session id] — post a grill question card to the UI (DESIGN.md §6, §8).
|
|
3
|
+
// Prints the question id; the agent then parks in `otacon wait` and the
|
|
4
|
+
// user's answer arrives as an {"event":"answer"} on stdout there.
|
|
5
|
+
//
|
|
6
|
+
// `--batch <file|->` posts several INDEPENDENT questions in one call (a JSON
|
|
7
|
+
// array of the same specs): the daemon mints them atomically and they render
|
|
8
|
+
// as ordinary cards, each answered instantly. Dependency-first grilling still
|
|
9
|
+
// holds — only independent siblings batch (DESIGN.md §8).
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
import { parseArgs } from "node:util";
|
|
13
|
+
import { parseQuestionSpec } from "../../shared/question-spec.js";
|
|
14
|
+
import { api, ensureDaemon } from "../client.js";
|
|
15
|
+
import { fail, printJson, usageError } from "../output.js";
|
|
16
|
+
import { listSessions, realpathOr, resolveSession } from "../session.js";
|
|
17
|
+
/**
|
|
18
|
+
* Hold one question body to the shared validator (DESIGN.md §6, §8) — the same
|
|
19
|
+
* rules the daemon re-checks — turning its error string into a usage error
|
|
20
|
+
* (E_USAGE, exit 2) carrying the caller's context: a `--batch[i] ` index, or
|
|
21
|
+
* "" for the bare flag form.
|
|
22
|
+
*/
|
|
23
|
+
function specOrUsage(raw, where) {
|
|
24
|
+
const spec = parseQuestionSpec(raw);
|
|
25
|
+
if (typeof spec === "string")
|
|
26
|
+
usageError(`${where}${spec}`);
|
|
27
|
+
return spec;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse and validate a --batch payload: a non-empty JSON array of question
|
|
31
|
+
* specs. Throws a usage error on a malformed array or member (naming its
|
|
32
|
+
* index) so the agent fixes its file before the daemon sees it; the daemon
|
|
33
|
+
* re-validates and mints the whole batch atomically (a bad member fails it
|
|
34
|
+
* all — no partial queue).
|
|
35
|
+
*/
|
|
36
|
+
export function parseBatch(content) {
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(content);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
usageError("--batch must be a JSON array of question objects");
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
45
|
+
usageError("--batch must be a non-empty JSON array of question objects");
|
|
46
|
+
}
|
|
47
|
+
return parsed.map((raw, i) => specOrUsage(raw, `--batch[${i}] `));
|
|
48
|
+
}
|
|
49
|
+
export async function askCommand(argv) {
|
|
50
|
+
const { values } = parseArgs({
|
|
51
|
+
args: argv,
|
|
52
|
+
options: {
|
|
53
|
+
question: { type: "string" },
|
|
54
|
+
options: { type: "string" },
|
|
55
|
+
recommend: { type: "string" },
|
|
56
|
+
multi: { type: "boolean", default: false },
|
|
57
|
+
session: { type: "string" },
|
|
58
|
+
batch: { type: "string" },
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
let payload;
|
|
62
|
+
if (values.batch !== undefined) {
|
|
63
|
+
if (values.question !== undefined ||
|
|
64
|
+
values.options !== undefined ||
|
|
65
|
+
values.recommend !== undefined ||
|
|
66
|
+
values.multi === true) {
|
|
67
|
+
usageError("--batch is exclusive with --question/--options/--recommend/--multi");
|
|
68
|
+
}
|
|
69
|
+
let content;
|
|
70
|
+
try {
|
|
71
|
+
content = readFileSync(values.batch === "-" ? 0 : resolve(values.batch), "utf8");
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
fail("E_NO_BATCH", `cannot read --batch ${values.batch}; write the questions there or pass -`);
|
|
75
|
+
}
|
|
76
|
+
payload = { questions: parseBatch(content) };
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
if (values.question === undefined || values.question.trim() === "") {
|
|
80
|
+
usageError('otacon ask requires --question "…" (or --batch <file|->)');
|
|
81
|
+
}
|
|
82
|
+
// Assemble the same spec a --batch member is, then hold it to the one shared
|
|
83
|
+
// validator — the only flag-specific bit is splitting --options on "|".
|
|
84
|
+
payload = {
|
|
85
|
+
...specOrUsage({
|
|
86
|
+
question: values.question,
|
|
87
|
+
...(values.options !== undefined
|
|
88
|
+
? { options: values.options.split("|").map((o) => o.trim()) }
|
|
89
|
+
: {}),
|
|
90
|
+
...(values.recommend !== undefined ? { recommend: values.recommend } : {}),
|
|
91
|
+
...(values.multi === true ? { multi: true } : {}),
|
|
92
|
+
}, ""),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
await ensureDaemon();
|
|
96
|
+
const session = resolveSession(await listSessions(), values.session, realpathOr(process.cwd()));
|
|
97
|
+
const response = await api("POST", `/api/sessions/${session.id}/ask`, payload);
|
|
98
|
+
if (response.status === 201) {
|
|
99
|
+
// single: {ok, session, id: "q<n>"}; batch: {ok, session, ids: [...]} —
|
|
100
|
+
// now park in `otacon wait`, looping it to drain a batch.
|
|
101
|
+
printJson(response.body);
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
const code = response.body.error?.code;
|
|
105
|
+
const message = response.body.error?.message;
|
|
106
|
+
if (response.status === 409) {
|
|
107
|
+
fail(code ?? "E_SESSION_OVER", message ?? `session ${session.id} is over`);
|
|
108
|
+
}
|
|
109
|
+
if (response.status === 404) {
|
|
110
|
+
fail("E_UNKNOWN_SESSION", `daemon no longer knows session ${session.id}`);
|
|
111
|
+
}
|
|
112
|
+
if (response.status === 400) {
|
|
113
|
+
fail("E_BAD_REQUEST", message ?? "daemon rejected the question");
|
|
114
|
+
}
|
|
115
|
+
fail("E_INTERNAL", `ask failed: ${JSON.stringify(response.body)}`, undefined, 2);
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=ask.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ask.js","sourceRoot":"","sources":["../../../src/cli/commands/ask.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// otacon clean [--all] — archive working state for ended sessions (DESIGN.md
|
|
2
|
+
// §6, §12): for every terminal session in this repo (--all: everywhere), it
|
|
3
|
+
// calls DELETE /api/sessions/:id; the daemon deregisters the session and
|
|
4
|
+
// archives its .otacon/<id>/ dir to .otacon/archive/<id>/, reporting the
|
|
5
|
+
// destination as `archivedTo` (the review UI drives the same route — terminal
|
|
6
|
+
// archives, non-terminal hard-deletes). Committed artifacts under docs/plans/
|
|
7
|
+
// are never touched (DECISIONS.md "clean: daemon deregisters and archives").
|
|
8
|
+
import { parseArgs } from "node:util";
|
|
9
|
+
import { TERMINAL_STATUSES } from "../../shared/types.js";
|
|
10
|
+
import { api, ensureDaemon } from "../client.js";
|
|
11
|
+
import { notice, printJson } from "../output.js";
|
|
12
|
+
import { findRepoRoot, listSessions, realpathOr } from "../session.js";
|
|
13
|
+
export async function cleanCommand(argv) {
|
|
14
|
+
const { values } = parseArgs({
|
|
15
|
+
args: argv,
|
|
16
|
+
options: { all: { type: "boolean", default: false } },
|
|
17
|
+
});
|
|
18
|
+
await ensureDaemon();
|
|
19
|
+
const cwd = realpathOr(process.cwd());
|
|
20
|
+
const root = findRepoRoot(cwd) ?? cwd;
|
|
21
|
+
// Only terminal (ended) sessions qualify — approved, plus implemented /
|
|
22
|
+
// implement_failed once a build finishes (DESIGN.md §12). A terminal session
|
|
23
|
+
// stays terminal, so clean's DELETE always takes the daemon's archive branch,
|
|
24
|
+
// never the non-terminal hard-delete one — a racing status change cannot
|
|
25
|
+
// sweep a live (including `implementing`) session.
|
|
26
|
+
const targets = (await listSessions()).filter((s) => TERMINAL_STATUSES.includes(s.status) && (values.all || realpathOr(s.repo) === root));
|
|
27
|
+
const cleaned = [];
|
|
28
|
+
for (const session of targets) {
|
|
29
|
+
const response = await api("DELETE", `/api/sessions/${session.id}`);
|
|
30
|
+
if (response.status !== 200) {
|
|
31
|
+
notice(`skipping ${session.id}: ${JSON.stringify(response.body)}`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const pending = response.body.pendingEvents;
|
|
35
|
+
if (typeof pending === "number" && pending > 0) {
|
|
36
|
+
notice(`${session.id}: ${pending} undelivered event(s) archived with it`);
|
|
37
|
+
}
|
|
38
|
+
// The daemon archived the dir and tells us where (null only if it was gone).
|
|
39
|
+
const archivedTo = response.body.archivedTo ?? null;
|
|
40
|
+
cleaned.push({ session: session.id, title: session.title, repo: session.repo, archivedTo });
|
|
41
|
+
}
|
|
42
|
+
if (cleaned.length === 0) {
|
|
43
|
+
notice(values.all ? "no ended sessions to clean" : `no ended sessions for ${root} (try --all)`);
|
|
44
|
+
}
|
|
45
|
+
printJson({ ok: true, cleaned });
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=clean.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clean.js","sourceRoot":"","sources":["../../../src/cli/commands/clean.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,4EAA4E;AAC5E,yEAAyE;AACzE,yEAAyE;AACzE,8EAA8E;AAC9E,8EAA8E;AAC9E,6EAA6E;AAE7E,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEvE,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAc;IAC/C,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;QAC3B,IAAI,EAAE,IAAI;QACV,OAAO,EAAE,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;KACtD,CAAC,CAAC;IACH,MAAM,YAAY,EAAE,CAAC;IACrB,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC;IACtC,wEAAwE;IACxE,6EAA6E;IAC7E,8EAA8E;IAC9E,yEAAyE;IACzE,mDAAmD;IACnD,MAAM,OAAO,GAAG,CAAC,MAAM,YAAY,EAAE,CAAC,CAAC,MAAM,CAC3C,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,CAC3F,CAAC;IAEF,MAAM,OAAO,GAAkF,EAAE,CAAC;IAClG,KAAK,MAAM,OAAO,IAAI,OAAO,EAAE,CAAC;QAC9B,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,QAAQ,EAAE,iBAAiB,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;QACpE,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC5B,MAAM,CAAC,YAAY,OAAO,CAAC,EAAE,KAAK,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACnE,SAAS;QACX,CAAC;QACD,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC;QAC5C,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;YAC/C,MAAM,CAAC,GAAG,OAAO,CAAC,EAAE,KAAK,OAAO,wCAAwC,CAAC,CAAC;QAC5E,CAAC;QACD,6EAA6E;QAC7E,MAAM,UAAU,GAAI,QAAQ,CAAC,IAAI,CAAC,UAAwC,IAAI,IAAI,CAAC;QACnF,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;IAC9F,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,4BAA4B,CAAC,CAAC,CAAC,yBAAyB,IAAI,cAAc,CAAC,CAAC;IAClG,CAAC;IACD,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IACjC,OAAO,CAAC,CAAC;AACX,CAAC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// otacon doctor — verify the machine setup (DESIGN.md §16): Node version,
|
|
2
|
+
// daemon boots and the port is free-or-ours (ensureDaemon does both), wrapper
|
|
3
|
+
// presence per agent, Stop hook registration, Tailscale state. Hard checks
|
|
4
|
+
// (node, daemon) fail the run with exit 1; everything optional — wrappers for
|
|
5
|
+
// agents the user may not use, phone access — is a warning, never a failure.
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
8
|
+
import { otaconPort } from "../../shared/paths.js";
|
|
9
|
+
import { ensureDaemon } from "../client.js";
|
|
10
|
+
import { CODEX_BEGIN, MANAGED_MARKER } from "../install/assets.js";
|
|
11
|
+
import { claudeHookScriptPath, claudeSkillPath, codexAgentsPath, opencodeSkillPath, settingsRegisterStopHook, } from "../install/locations.js";
|
|
12
|
+
import { findTailscale, tailscaleStatus } from "../install/tailscale.js";
|
|
13
|
+
import { CliError, printJson } from "../output.js";
|
|
14
|
+
function wrapperCheck(name, path, marker) {
|
|
15
|
+
const present = existsSync(path) && readFileSync(path, "utf8").includes(marker);
|
|
16
|
+
return present
|
|
17
|
+
? { name, status: "ok", detail: path }
|
|
18
|
+
: {
|
|
19
|
+
name,
|
|
20
|
+
status: "warn",
|
|
21
|
+
detail: `wrapper not installed at ${path}; run otacon install --agent ${name.replace("wrapper-", "")}`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function stopHookCheck() {
|
|
25
|
+
const name = "stop-hook";
|
|
26
|
+
if (settingsRegisterStopHook()) {
|
|
27
|
+
return { name, status: "ok", detail: claudeHookScriptPath() };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
name,
|
|
31
|
+
status: "warn",
|
|
32
|
+
detail: "Stop hook not registered; run otacon install --agent claude --hooks",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function tailscaleCheck() {
|
|
36
|
+
const name = "tailscale";
|
|
37
|
+
const bin = findTailscale();
|
|
38
|
+
if (bin === undefined) {
|
|
39
|
+
return {
|
|
40
|
+
name,
|
|
41
|
+
status: "warn",
|
|
42
|
+
detail: "tailscale CLI not found — phone access is optional (DESIGN.md §11; otacon expose)",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const status = tailscaleStatus(bin);
|
|
46
|
+
if (status?.backendState === "Running") {
|
|
47
|
+
return { name, status: "ok", detail: `${bin} (${status.dnsName ?? "no MagicDNS name"})` };
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
name,
|
|
51
|
+
status: "warn",
|
|
52
|
+
detail: `tailscale backend is ${status?.backendState ?? "unreachable"}; run \`tailscale up\` before otacon expose`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export async function doctorCommand(argv) {
|
|
56
|
+
parseArgs({ args: argv, options: {} });
|
|
57
|
+
const checks = [];
|
|
58
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
59
|
+
checks.push(nodeMajor >= 20
|
|
60
|
+
? { name: "node", status: "ok", detail: `node ${process.versions.node}` }
|
|
61
|
+
: { name: "node", status: "fail", detail: `node ${process.versions.node} — otacon needs >=20` });
|
|
62
|
+
try {
|
|
63
|
+
const health = await ensureDaemon();
|
|
64
|
+
checks.push({
|
|
65
|
+
name: "daemon",
|
|
66
|
+
status: "ok",
|
|
67
|
+
detail: `otacond ${health.version} (pid ${health.pid}) on port ${otaconPort()}`,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
checks.push({
|
|
72
|
+
name: "daemon",
|
|
73
|
+
status: "fail",
|
|
74
|
+
detail: error instanceof CliError ? `${error.code}: ${error.message}` : String(error),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
checks.push(wrapperCheck("wrapper-claude", claudeSkillPath(), MANAGED_MARKER));
|
|
78
|
+
checks.push(wrapperCheck("wrapper-codex", codexAgentsPath(), CODEX_BEGIN));
|
|
79
|
+
checks.push(wrapperCheck("wrapper-opencode", opencodeSkillPath(), MANAGED_MARKER));
|
|
80
|
+
checks.push(stopHookCheck());
|
|
81
|
+
checks.push(tailscaleCheck());
|
|
82
|
+
const ok = checks.every((c) => c.status !== "fail");
|
|
83
|
+
printJson({ ok, checks });
|
|
84
|
+
return ok ? 0 : 1;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=doctor.js.map
|