quadwork 1.19.2 → 2.0.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 (117) hide show
  1. package/README.md +19 -35
  2. package/bin/quadwork.js +48 -1118
  3. package/out/404.html +1 -1
  4. package/out/__next.__PAGE__.txt +3 -3
  5. package/out/__next._full.txt +14 -14
  6. package/out/__next._head.txt +4 -4
  7. package/out/__next._index.txt +8 -8
  8. package/out/__next._tree.txt +2 -2
  9. package/out/_next/static/chunks/{030cjkhts487t.js → 079wdniva~de1.js} +1 -1
  10. package/out/_next/static/chunks/{0n~dq4kpx9xxx.js → 07lhk_q6pmm3r.js} +1 -1
  11. package/out/_next/static/chunks/0_79hkefw1mo2.js +1 -0
  12. package/out/_next/static/chunks/{153f.fj8jlvle.js → 0_lyyn..t63bc.js} +1 -1
  13. package/out/_next/static/chunks/0oxv9vrvc17to.js +2 -0
  14. package/out/_next/static/chunks/0py7102i226n5.js +1 -0
  15. package/out/_next/static/chunks/{13fv-yi7.v52g.js → 0q4bm04c1jl_3.js} +1 -1
  16. package/out/_next/static/chunks/{0_idxioyl0p7h.js → 0sjhy6oe3mbon.js} +1 -1
  17. package/out/_next/static/chunks/13xk0vgfbrcld.css +2 -0
  18. package/out/_next/static/chunks/14k3bfe537f9_.js +25 -0
  19. package/out/_next/static/chunks/{turbopack-0qm-e3ifrz~2u.js → turbopack-0y2u-q0l2m67w.js} +1 -1
  20. package/out/_not-found/__next._full.txt +13 -13
  21. package/out/_not-found/__next._head.txt +4 -4
  22. package/out/_not-found/__next._index.txt +8 -8
  23. package/out/_not-found/__next._not-found.__PAGE__.txt +2 -2
  24. package/out/_not-found/__next._not-found.txt +3 -3
  25. package/out/_not-found/__next._tree.txt +2 -2
  26. package/out/_not-found.html +1 -1
  27. package/out/_not-found.txt +13 -13
  28. package/out/app-shell/__next._full.txt +13 -13
  29. package/out/app-shell/__next._head.txt +4 -4
  30. package/out/app-shell/__next._index.txt +8 -8
  31. package/out/app-shell/__next._tree.txt +2 -2
  32. package/out/app-shell/__next.app-shell.__PAGE__.txt +2 -2
  33. package/out/app-shell/__next.app-shell.txt +3 -3
  34. package/out/app-shell.html +1 -1
  35. package/out/app-shell.txt +13 -13
  36. package/out/index.html +1 -1
  37. package/out/index.txt +14 -14
  38. package/out/project/_/__next._full.txt +14 -14
  39. package/out/project/_/__next._head.txt +4 -4
  40. package/out/project/_/__next._index.txt +8 -8
  41. package/out/project/_/__next._tree.txt +2 -2
  42. package/out/project/_/__next.project.$d$id.__PAGE__.txt +3 -3
  43. package/out/project/_/__next.project.$d$id.txt +3 -3
  44. package/out/project/_/__next.project.txt +3 -3
  45. package/out/project/_/queue/__next._full.txt +14 -14
  46. package/out/project/_/queue/__next._head.txt +4 -4
  47. package/out/project/_/queue/__next._index.txt +8 -8
  48. package/out/project/_/queue/__next._tree.txt +2 -2
  49. package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +3 -3
  50. package/out/project/_/queue/__next.project.$d$id.queue.txt +3 -3
  51. package/out/project/_/queue/__next.project.$d$id.txt +3 -3
  52. package/out/project/_/queue/__next.project.txt +3 -3
  53. package/out/project/_/queue.html +1 -1
  54. package/out/project/_/queue.txt +14 -14
  55. package/out/project/_.html +1 -1
  56. package/out/project/_.txt +14 -14
  57. package/out/settings/__next._full.txt +14 -14
  58. package/out/settings/__next._head.txt +4 -4
  59. package/out/settings/__next._index.txt +8 -8
  60. package/out/settings/__next._tree.txt +2 -2
  61. package/out/settings/__next.settings.__PAGE__.txt +3 -3
  62. package/out/settings/__next.settings.txt +3 -3
  63. package/out/settings.html +1 -1
  64. package/out/settings.txt +14 -14
  65. package/out/setup/__next._full.txt +14 -14
  66. package/out/setup/__next._head.txt +4 -4
  67. package/out/setup/__next._index.txt +8 -8
  68. package/out/setup/__next._tree.txt +2 -2
  69. package/out/setup/__next.setup.__PAGE__.txt +3 -3
  70. package/out/setup/__next.setup.txt +3 -3
  71. package/out/setup.html +1 -1
  72. package/out/setup.txt +14 -14
  73. package/package.json +4 -2
  74. package/server/ac-restore.js +128 -0
  75. package/server/bridges/discord.js +183 -0
  76. package/server/bridges/telegram.js +210 -0
  77. package/server/config.js +4 -60
  78. package/server/file-chat.js +318 -0
  79. package/server/index.js +173 -1286
  80. package/server/install-agentchattr.js +3 -284
  81. package/server/mcp-chat-shim.js +171 -0
  82. package/server/migrate-ac.js +158 -0
  83. package/server/pty-dispatcher.js +188 -0
  84. package/server/routes.js +149 -1397
  85. package/templates/CLAUDE.md +2 -2
  86. package/templates/OVERNIGHT-QUEUE.md +1 -1
  87. package/templates/seeds/butler.CLAUDE.md +30 -62
  88. package/templates/seeds/dev.AGENTS.md +10 -1
  89. package/templates/seeds/head.AGENTS.md +3 -3
  90. package/templates/seeds/re1.AGENTS.md +3 -3
  91. package/templates/seeds/re2.AGENTS.md +3 -3
  92. package/bridges/discord/__pycache__/discord_bridge.cpython-314.pyc +0 -0
  93. package/bridges/discord/discord_bridge.py +0 -666
  94. package/bridges/discord/requirements.txt +0 -2
  95. package/out/_next/static/chunks/0_bb~2.5h2ntm.css +0 -2
  96. package/out/_next/static/chunks/0makcdqkwobp6.js +0 -25
  97. package/out/_next/static/chunks/0uz5svjlo9dwl.js +0 -1
  98. package/out/_next/static/chunks/0zahstmgdrpy5.js +0 -1
  99. package/out/_next/static/chunks/0zfotsowwll1x.js +0 -2
  100. package/server/__tests__/bridge-auto-stop-guard.test.js +0 -134
  101. package/server/__tests__/rate-limit-handling.test.js +0 -168
  102. package/server/__tests__/scrub-secrets.test.js +0 -235
  103. package/server/__tests__/v1110-security-qa.test.js +0 -312
  104. package/server/agentchattr-registry.js +0 -188
  105. package/server/install-agentchattr.patchCrashTimeout.test.js +0 -71
  106. package/server/queue-watcher.js +0 -171
  107. package/server/queue-watcher.test.js +0 -64
  108. package/server/routes.batchProgress.test.js +0 -94
  109. package/server/routes.chatWsSend.test.js +0 -161
  110. package/server/routes.discordBridge.test.js +0 -80
  111. package/server/routes.parseActiveBatch.test.js +0 -88
  112. package/server/routes.telegramBridge.test.js +0 -241
  113. package/templates/config.toml +0 -72
  114. package/templates/wrapper.py +0 -70
  115. /package/out/_next/static/{K7A3YZrh4sLaRRP1-Lq7v → 479UD5Kit4YvCmtgO25VT}/_buildManifest.js +0 -0
  116. /package/out/_next/static/{K7A3YZrh4sLaRRP1-Lq7v → 479UD5Kit4YvCmtgO25VT}/_clientMiddlewareManifest.js +0 -0
  117. /package/out/_next/static/{K7A3YZrh4sLaRRP1-Lq7v → 479UD5Kit4YvCmtgO25VT}/_ssgManifest.js +0 -0
@@ -1,241 +0,0 @@
1
- // #353 / quadwork#353: readLastLines helper test. Plain
2
- // node:assert script — run with
3
- // `node server/routes.telegramBridge.test.js`.
4
- //
5
- // The bridge start/install handlers are integration-shaped (they
6
- // spawn python3), so this file covers only the pure log-tailing
7
- // helper that the handlers rely on for error reporting.
8
-
9
- const assert = require("node:assert/strict");
10
- const fs = require("node:fs");
11
- const os = require("node:os");
12
- const path = require("node:path");
13
- const { execFileSync } = require("node:child_process");
14
- const {
15
- readLastLines,
16
- checkTelegramBridgePythonDeps,
17
- resolveProjectAgentchattrUrl,
18
- buildTelegramBridgeToml,
19
- patchAgentchattrConfigForTelegramBridge,
20
- buildTelegramBridgeSpawnEnv,
21
- } = require("./routes");
22
-
23
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "qw-bridge-log-"));
24
- function write(name, content) {
25
- const p = path.join(tmp, name);
26
- fs.writeFileSync(p, content);
27
- return p;
28
- }
29
-
30
- try {
31
- // 1) Missing file returns empty string.
32
- assert.equal(readLastLines(path.join(tmp, "missing.log"), 5), "");
33
-
34
- // 2) Empty file returns empty string.
35
- assert.equal(readLastLines(write("empty.log", ""), 5), "");
36
-
37
- // 3) Fewer lines than N → return them all, joined with \n.
38
- assert.equal(
39
- readLastLines(write("two.log", "a\nb\n"), 5),
40
- "a\nb",
41
- );
42
-
43
- // 4) More lines than N → return only the last N in order.
44
- assert.equal(
45
- readLastLines(write("many.log", "a\nb\nc\nd\ne\nf\n"), 3),
46
- "d\ne\nf",
47
- );
48
-
49
- // 5) \r\n line endings handled.
50
- assert.equal(
51
- readLastLines(write("crlf.log", "x\r\ny\r\nz\r\n"), 2),
52
- "y\nz",
53
- );
54
-
55
- // 6) Blank lines inside the tail are skipped (filter non-empty).
56
- // This matches readLastLines' `.filter((l) => l.length > 0)`.
57
- assert.equal(
58
- readLastLines(write("blanks.log", "a\n\nb\n\n\nc\n"), 2),
59
- "b\nc",
60
- );
61
-
62
- // 7) Simulated crash trace — the caller should get the final
63
- // frame so the operator can see the ModuleNotFoundError.
64
- const crash = [
65
- "Traceback (most recent call last):",
66
- ' File "telegram_bridge.py", line 3, in <module>',
67
- " import requests",
68
- "ModuleNotFoundError: No module named 'requests'",
69
- "",
70
- ].join("\n");
71
- const got = readLastLines(write("crash.log", crash), 20);
72
- assert.match(got, /ModuleNotFoundError/);
73
- assert.match(got, /No module named 'requests'/);
74
-
75
- // 8) #372: pre-flight dep-check failures must be persisted to
76
- // the bridge log file so a subsequent status poll can surface
77
- // them via last_error. Without this the widget's local
78
- // actionError got clobbered by the next 5s polling cycle and
79
- // the failure appeared as a silent Start → Stopped flicker.
80
- // Here we simulate the exact fs.writeFileSync the start
81
- // handler now runs on pre-flight failure, then round-trip it
82
- // through readLastLines to confirm the error text survives.
83
- const preflightLog = path.join(tmp, "preflight.log");
84
- const msg =
85
- "Bridge Python dependencies not installed. Click \"Install Bridge\" to install them, " +
86
- "or run: pip3 install -r /tmp/fake/requirements.txt\n\n" +
87
- "Import error: Traceback (most recent call last):\n" +
88
- " File \"<string>\", line 1, in <module>\n" +
89
- " import requests\n" +
90
- "ModuleNotFoundError: No module named 'requests'";
91
- fs.writeFileSync(
92
- preflightLog,
93
- `[${new Date().toISOString()}] pre-flight dep check failed\n${msg}\n`,
94
- );
95
- const tail = readLastLines(preflightLog, 20);
96
- assert.match(tail, /pre-flight dep check failed/);
97
- assert.match(tail, /ModuleNotFoundError/);
98
- assert.match(tail, /Install Bridge/);
99
-
100
- // 9) #380: checkTelegramBridgePythonDeps accepts an explicit
101
- // interpreter path. Passing a guaranteed-broken path must
102
- // return { ok: false, error } without throwing.
103
- const broken = checkTelegramBridgePythonDeps(path.join(tmp, "nope", "python3"));
104
- assert.equal(broken.ok, false);
105
- assert.ok(broken.error && broken.error.length > 0);
106
-
107
- // 10) #380: start handler's missing-venv branch — we don't boot
108
- // the server here, but the branch reduces to a plain
109
- // fs.existsSync check on `<BRIDGE_DIR>/.venv/bin/python3`,
110
- // so we verify the check returns false for a fixture dir
111
- // that has no `.venv` subdir at all.
112
- const fixtureBridgeDir = fs.mkdtempSync(path.join(tmp, "bridge-no-venv-"));
113
- const missingVenvPython = path.join(fixtureBridgeDir, ".venv", "bin", "python3");
114
- assert.equal(fs.existsSync(missingVenvPython), false);
115
-
116
- // 11) #380: round-trip — build a real venv in a tmp dir, install
117
- // a stdlib-only sentinel is trivially importable, and confirm
118
- // checkTelegramBridgePythonDeps reports ok when `requests`
119
- // is installed into that venv. Skipped gracefully on CI if
120
- // `python3 -m venv` or network-backed pip install fails.
121
- const venvDir = path.join(tmp, "case11-venv");
122
- let venvSkipped = false;
123
- try {
124
- execFileSync("python3", ["-m", "venv", venvDir], { timeout: 30000, stdio: "pipe" });
125
- const venvPython = path.join(venvDir, "bin", "python3");
126
- const venvPip = path.join(venvDir, "bin", "pip");
127
- // Without `requests` installed yet, the check must fail.
128
- const before = checkTelegramBridgePythonDeps(venvPython);
129
- assert.equal(before.ok, false);
130
- try {
131
- execFileSync(venvPip, ["install", "--quiet", "requests"], { timeout: 120000, stdio: "pipe" });
132
- } catch {
133
- venvSkipped = true;
134
- }
135
- if (!venvSkipped) {
136
- const after = checkTelegramBridgePythonDeps(venvPython);
137
- assert.equal(after.ok, true);
138
- }
139
- } catch {
140
- venvSkipped = true;
141
- }
142
-
143
- // 12) #383 Bug 1: resolveProjectAgentchattrUrl prefers the
144
- // per-project URL over the global default. Every project
145
- // after the first uses a distinct port, so silently reading
146
- // the global default routed bridge traffic to the wrong AC
147
- // instance.
148
- assert.equal(
149
- resolveProjectAgentchattrUrl(
150
- { agentchattr_url: "http://127.0.0.1:8300" },
151
- { id: "quadwork", agentchattr_url: "http://127.0.0.1:8301" },
152
- ),
153
- "http://127.0.0.1:8301",
154
- );
155
- // Falls back to global default when the project has no URL of
156
- // its own (legacy single-project installs).
157
- assert.equal(
158
- resolveProjectAgentchattrUrl(
159
- { agentchattr_url: "http://127.0.0.1:8300" },
160
- { id: "legacy" },
161
- ),
162
- "http://127.0.0.1:8300",
163
- );
164
- // Hard-coded fallback when neither is set.
165
- assert.equal(
166
- resolveProjectAgentchattrUrl({}, {}),
167
- "http://127.0.0.1:8300",
168
- );
169
-
170
- // 13) #383 Bug 2: buildTelegramBridgeToml writes agentchattr_url
171
- // inside [telegram]. The upstream bridge's load_config only
172
- // reads from that section — a separate [agentchattr] section
173
- // is silently ignored and the bridge falls back to its
174
- // hardcoded :8300 default.
175
- const toml13 = buildTelegramBridgeToml({
176
- bot_token: "123:abc",
177
- chat_id: "-42",
178
- agentchattr_url: "http://127.0.0.1:8301",
179
- }, "testproject");
180
- assert.match(toml13, /^\[telegram\]/);
181
- assert.match(toml13, /bot_token = "123:abc"/);
182
- assert.match(toml13, /chat_id = "-42"/);
183
- assert.match(toml13, /agentchattr_url = "http:\/\/127\.0\.0\.1:8301"/);
184
- // #404: cursor_file must be per-project so multiple bridges
185
- // don't clobber each other's position.
186
- assert.match(toml13, /cursor_file = ".*tg-bridge-cursor-testproject\.json"/);
187
- // Must NOT emit a separate [agentchattr] section — the bridge
188
- // would silently ignore it.
189
- assert.equal(toml13.includes("\n[agentchattr]\n"), false);
190
-
191
- // 14) #383 Bug 3: patchAgentchattrConfigForTelegramBridge is
192
- // idempotent. The Install Bridge migration may run multiple
193
- // times; it must not duplicate the section or corrupt the
194
- // file.
195
- const baseConfig =
196
- "[agents.head]\nlabel = \"Head\"\n\n[agents.dev]\nlabel = \"Dev\"\n";
197
- const first = patchAgentchattrConfigForTelegramBridge(baseConfig);
198
- assert.equal(first.changed, true);
199
- assert.match(first.text, /^\[agents\.tg\]$/m);
200
- assert.match(first.text, /label = "Telegram Bridge"/);
201
- // Running a second time is a no-op.
202
- const second = patchAgentchattrConfigForTelegramBridge(first.text);
203
- assert.equal(second.changed, false);
204
- assert.equal(second.text, first.text);
205
- // #439: a config with old slug [agents.telegram-bridge] is migrated
206
- // to [agents.tg] and flagged as changed.
207
- const handPatched =
208
- baseConfig + "\n[agents.telegram-bridge]\nlabel = \"Telegram Bridge\"\n";
209
- const third = patchAgentchattrConfigForTelegramBridge(handPatched);
210
- assert.equal(third.changed, true);
211
- assert.match(third.text, /^\[agents\.tg\]$/m);
212
-
213
- // 15) #383 Bug 4: buildTelegramBridgeSpawnEnv strips the three
214
- // env vars the upstream bridge treats as higher-precedence
215
- // than TOML. Without this, an operator shell that exported a
216
- // different bot's token (common on machines running AC2)
217
- // silently overrode the QuadWork-written TOML and the bridge
218
- // ran as the wrong identity.
219
- const scrubbed = buildTelegramBridgeSpawnEnv({
220
- PATH: "/usr/bin",
221
- HOME: "/home/op",
222
- TELEGRAM_BOT_TOKEN: "wrong-token",
223
- TELEGRAM_CHAT_ID: "-999",
224
- AGENTCHATTR_URL: "http://127.0.0.1:9999",
225
- });
226
- assert.equal(scrubbed.TELEGRAM_BOT_TOKEN, undefined);
227
- assert.equal(scrubbed.TELEGRAM_CHAT_ID, undefined);
228
- assert.equal(scrubbed.AGENTCHATTR_URL, undefined);
229
- // Non-telegram keys must pass through untouched — the bridge
230
- // still needs PATH/HOME/etc. to find python and open files.
231
- assert.equal(scrubbed.PATH, "/usr/bin");
232
- assert.equal(scrubbed.HOME, "/home/op");
233
-
234
- console.log(
235
- "routes.telegramBridge.test.js: all assertions passed (15 cases" +
236
- (venvSkipped ? ", case 11 pip step skipped" : "") +
237
- ")",
238
- );
239
- } finally {
240
- try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {}
241
- }
@@ -1,72 +0,0 @@
1
- # QuadWork AgentChattr Configuration
2
- # Generated by: npx quadwork init
3
- #
4
- # Agent "command" is the CLI tool: "claude", "codex", "gemini", or a script path.
5
- # "cwd" is the working directory (git worktree) for each agent.
6
- # MCP injection is auto-detected from the command name (claude/codex/gemini).
7
-
8
- [meta]
9
- version = "1.0.0"
10
-
11
- [server]
12
- port = 8300
13
- host = "127.0.0.1"
14
- data_dir = "./data"
15
-
16
- [agents.head]
17
- command = "codex"
18
- cwd = "{{head_cwd}}"
19
- color = "#10a37f"
20
- label = "Lead"
21
-
22
- [agents.re1]
23
- command = "codex"
24
- cwd = "{{re1_cwd}}"
25
- color = "#22c55e"
26
- label = "Reviewer 1"
27
-
28
- [agents.re2]
29
- command = "claude"
30
- cwd = "{{re2_cwd}}"
31
- color = "#f59e0b"
32
- label = "Reviewer 2"
33
-
34
- [agents.dev]
35
- command = "claude"
36
- cwd = "{{dev_cwd}}"
37
- color = "#da7756"
38
- label = "Builder"
39
-
40
- # #592: CLI-based agent sections for AC HEAD compatibility.
41
- # HEAD AC validates `base` against [agents.*] keys in config.toml.
42
- # These sections let registration succeed with CLI names as base,
43
- # regardless of whether AC is running pinned or at HEAD.
44
- [agents.claude]
45
- command = "claude"
46
- label = "claude"
47
- mcp_inject = "flag"
48
-
49
- [agents.codex]
50
- command = "codex"
51
- label = "codex"
52
- mcp_inject = "proxy_flag"
53
-
54
- # #383: AC's registry rejects bases not declared in config.toml.
55
- # The Telegram bridge registers as `tg` (#439: renamed from
56
- # `telegram-bridge`), so every per-project AC config must declare
57
- # it. The bridge has no command/cwd of its own — it is a long-running
58
- # external client that posts to AC's HTTP API.
59
- [agents.tg]
60
- label = "Telegram Bridge"
61
-
62
- # #399/#439: Discord bridge registers as `dc` (renamed from `discord-bridge`).
63
- [agents.dc]
64
- label = "Discord Bridge"
65
-
66
- [routing]
67
- default = "none"
68
- max_agent_hops = 30
69
-
70
- [mcp]
71
- http_port = 8200
72
- sse_port = 8201
@@ -1,70 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Agent process wrapper — manages lifecycle, auto-trigger, and REMINDER injection.
3
-
4
- Usage:
5
- python wrapper.py --agent <agent_id> --config <config_path> [--project <project_id>]
6
-
7
- This is an optional advanced template for automating agent process management.
8
- Copy to your project and customize as needed.
9
- """
10
-
11
- import argparse
12
- import json
13
- import os
14
- import signal
15
- import subprocess
16
- import sys
17
- import time
18
-
19
- def load_config(config_path: str) -> dict:
20
- with open(config_path) as f:
21
- return json.load(f)
22
-
23
- def resolve_agent(config: dict, project_id: str, agent_id: str) -> dict | None:
24
- for project in config.get("projects", []):
25
- if project.get("id") == project_id:
26
- return project.get("agents", {}).get(agent_id)
27
- return None
28
-
29
- def run_agent(agent: dict, agent_id: str) -> subprocess.Popen:
30
- command = agent.get("command", os.environ.get("SHELL", "/bin/zsh"))
31
- cwd = agent.get("cwd", os.getcwd())
32
- env = {**os.environ, "QUADWORK_AGENT": agent_id}
33
-
34
- proc = subprocess.Popen(
35
- [command],
36
- cwd=cwd,
37
- env=env,
38
- stdin=sys.stdin,
39
- stdout=sys.stdout,
40
- stderr=sys.stderr,
41
- )
42
- return proc
43
-
44
- def main():
45
- parser = argparse.ArgumentParser(description="Agent process wrapper")
46
- parser.add_argument("--agent", required=True, help="Agent ID (e.g. t3)")
47
- parser.add_argument("--config", required=True, help="Path to config.json")
48
- parser.add_argument("--project", default=None, help="Project ID (uses first project if omitted)")
49
- args = parser.parse_args()
50
-
51
- config = load_config(args.config)
52
- project_id = args.project or config.get("projects", [{}])[0].get("id", "")
53
- agent = resolve_agent(config, project_id, args.agent)
54
-
55
- if not agent:
56
- print(f"Agent '{args.agent}' not found in project '{project_id}'", file=sys.stderr)
57
- sys.exit(1)
58
-
59
- proc = run_agent(agent, args.agent)
60
-
61
- def handle_signal(signum, _frame):
62
- proc.send_signal(signum)
63
-
64
- signal.signal(signal.SIGTERM, handle_signal)
65
- signal.signal(signal.SIGINT, handle_signal)
66
-
67
- sys.exit(proc.wait())
68
-
69
- if __name__ == "__main__":
70
- main()