taskuary 0.3.1__tar.gz → 0.3.2__tar.gz
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.
- taskuary-0.3.2/PKG-INFO +165 -0
- taskuary-0.3.2/README.md +114 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/pyproject.toml +3 -1
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/__init__.py +1 -1
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/agents.py +198 -10
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/aisetup.py +3 -2
- taskuary-0.3.2/taskuary/assistant.py +850 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/blackboard.py +2 -2
- taskuary-0.3.2/taskuary/browserview.py +155 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/calendar.py +116 -12
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/categories.py +1 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/channels.py +56 -9
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/cli.py +1 -1
- taskuary-0.3.2/taskuary/climodels.py +57 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/clis.py +43 -4
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/coder.py +38 -3
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/compose.py +143 -38
- taskuary-0.3.2/taskuary/context.py +111 -0
- taskuary-0.3.2/taskuary/counsel.py +97 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/devtools.py +4 -2
- taskuary-0.3.2/taskuary/digest.py +286 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/docsync.py +13 -5
- taskuary-0.3.2/taskuary/general.py +417 -0
- taskuary-0.3.2/taskuary/handraise.py +59 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/histgen.py +91 -7
- taskuary-0.3.2/taskuary/hooks.py +83 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/imessage.py +3 -2
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/ingest.py +52 -23
- taskuary-0.3.2/taskuary/knowledge.py +317 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/llm.py +16 -8
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/messengers.py +103 -19
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/outbound.py +96 -17
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/phone.py +50 -12
- taskuary-0.3.2/taskuary/problems.py +29 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/promptmap.py +3 -1
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/rank.py +1 -1
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/reports.py +326 -37
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/responder.py +42 -9
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/scopes.py +8 -0
- taskuary-0.3.2/taskuary/semantic.py +370 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/server.py +817 -79
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/setup.py +32 -9
- taskuary-0.3.2/taskuary/sharepoint.py +136 -0
- taskuary-0.3.2/taskuary/sheets.py +101 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/store.py +380 -23
- taskuary-0.3.2/taskuary/templates/counsel.md +29 -0
- taskuary-0.3.2/taskuary/templates/digest.md +6 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/templates/triage.md +15 -5
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/terminal.py +135 -36
- taskuary-0.3.2/taskuary/testing.py +376 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/triage.py +57 -23
- taskuary-0.3.2/taskuary/voice.py +262 -0
- taskuary-0.3.2/taskuary/wabridge.py +145 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/waitroom.py +24 -5
- taskuary-0.3.2/taskuary/web/assets/GeneralWorkspace-D46ydeAJ.css +1 -0
- taskuary-0.3.2/taskuary/web/assets/GeneralWorkspace-Dn17SMoR.js +10 -0
- taskuary-0.3.2/taskuary/web/assets/index-zLivezbi.js +461 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/web/index.html +1 -1
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/whatsapp/bridge.mjs +28 -6
- taskuary-0.3.2/taskuary/whoami.py +156 -0
- taskuary-0.3.2/taskuary/witness.py +200 -0
- taskuary-0.3.2/taskuary.egg-info/PKG-INFO +165 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary.egg-info/SOURCES.txt +54 -2
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary.egg-info/requires.txt +4 -0
- taskuary-0.3.2/tests/test_agent_environment.py +111 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_agent_source.py +14 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_answers.py +37 -9
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_api.py +36 -17
- taskuary-0.3.2/tests/test_assistant.py +568 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_blackboard.py +3 -1
- taskuary-0.3.2/tests/test_browserview.py +159 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_calendar.py +2 -2
- taskuary-0.3.2/tests/test_calendar_today.py +77 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_categories.py +9 -9
- taskuary-0.3.2/tests/test_climodels.py +48 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_clis.py +19 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_compose.py +2 -1
- taskuary-0.3.2/tests/test_compose_sources.py +178 -0
- taskuary-0.3.2/tests/test_context.py +87 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_core.py +67 -18
- taskuary-0.3.2/tests/test_counsel.py +92 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_devtools.py +8 -8
- taskuary-0.3.2/tests/test_digest_brief.py +115 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_docs_and_llm.py +1 -1
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_docs_flow.py +1 -1
- taskuary-0.3.2/tests/test_duplicate_connectors.py +77 -0
- taskuary-0.3.2/tests/test_factory.py +226 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_feed_working.py +18 -11
- taskuary-0.3.2/tests/test_general.py +195 -0
- taskuary-0.3.2/tests/test_handraise.py +48 -0
- taskuary-0.3.2/tests/test_histgen.py +249 -0
- taskuary-0.3.2/tests/test_kind_dispatch.py +196 -0
- taskuary-0.3.2/tests/test_knowledge.py +205 -0
- taskuary-0.3.2/tests/test_mail_folders.py +83 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_messengers.py +40 -1
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_msauth.py +4 -4
- taskuary-0.3.2/tests/test_noop_ending.py +97 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_not_coding.py +56 -19
- taskuary-0.3.2/tests/test_problems.py +32 -0
- taskuary-0.3.2/tests/test_pypi_downloads.py +126 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_reply_voice.py +2 -2
- taskuary-0.3.2/tests/test_report_alerts.py +87 -0
- taskuary-0.3.2/tests/test_report_triage.py +58 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_reviews.py +3 -5
- taskuary-0.3.2/tests/test_seeded_reports.py +103 -0
- taskuary-0.3.2/tests/test_semantic.py +311 -0
- taskuary-0.3.2/tests/test_send_targets.py +124 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_setup.py +42 -1
- taskuary-0.3.2/tests/test_sheets_sharepoint.py +120 -0
- taskuary-0.3.2/tests/test_store_app_cli.py +100 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_store_hotpaths.py +47 -66
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_terminal.py +94 -1
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_urgent_and_handoff.py +1 -1
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_verdict_paths.py +2 -2
- taskuary-0.3.2/tests/test_voice.py +201 -0
- taskuary-0.3.2/tests/test_wabridge.py +103 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_waitroom.py +7 -1
- taskuary-0.3.2/tests/test_web_entrypoint.py +31 -0
- taskuary-0.3.2/tests/test_whoami.py +103 -0
- taskuary-0.3.2/tests/test_witness.py +106 -0
- taskuary-0.3.1/PKG-INFO +0 -526
- taskuary-0.3.1/README.md +0 -478
- taskuary-0.3.1/taskuary/digest.py +0 -163
- taskuary-0.3.1/taskuary/templates/digest.md +0 -5
- taskuary-0.3.1/taskuary/web/assets/index-fSY_EAQs.js +0 -441
- taskuary-0.3.1/taskuary.egg-info/PKG-INFO +0 -526
- taskuary-0.3.1/tests/test_histgen.py +0 -141
- {taskuary-0.3.1 → taskuary-0.3.2}/LICENSE +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/setup.cfg +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/artifacts.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/aws.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/azure.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/ci.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/config.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/db.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/desktop.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/evalset.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/github.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/imapmail.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/intacct.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/learn.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/learnedgraph.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/logs.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/mcp.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/msauth.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/mssql.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/pm.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/policy.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/proof.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/proposals.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/research.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/reshape.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/routing.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/senders.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/templates/coder.md +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/templates/learned.md +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/templates/soul.md +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/templates/style.md +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/toil.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/verdicts.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/web/assets/index-6GBZ9nXN.css +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/web/favicon.ico +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/web/favicon.png +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/webguard.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary/whatsapp/package.json +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary.egg-info/dependency_links.txt +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary.egg-info/entry_points.txt +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/taskuary.egg-info/top_level.txt +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_aisetup.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_async_triage.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_audit_chain.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_aws_region.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_cc_triage.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_cloud.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_cloud_sources.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_config.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_cwlogs_paging.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_desktop.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_diff_review.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_github_auto.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_harvest.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_imapmail.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_imessage.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_intacct.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_learnedgraph.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_local_file.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_mark_read.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_mcp.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_mssql.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_note_retrieval.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_outbound_reports.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_pm.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_poll_overlap.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_poll_sources.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_pr_comments.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_promptmap_and_catalog.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_proof_ci_proposals.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_rank.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_reply_channels.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_research.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_scopes.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_someone_else_replied.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_sync_clock.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_task_identity.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_toil.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_triage_dataset.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_verdict_sticks.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_vision.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_watermark.py +0 -0
- {taskuary-0.3.1 → taskuary-0.3.2}/tests/test_webguard.py +0 -0
taskuary-0.3.2/PKG-INFO
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: taskuary
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Automate your job - the local-first operating system for your work: AI triage in, coding agents out.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/ldbumble/taskuary
|
|
7
|
+
Project-URL: Repository, https://github.com/ldbumble/taskuary
|
|
8
|
+
Project-URL: Issues, https://github.com/ldbumble/taskuary/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/ldbumble/taskuary/releases
|
|
10
|
+
Keywords: ai,agents,automation,claude,coding-agent,email,local-first,task-management
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Office/Business :: Scheduling
|
|
19
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: fastapi>=0.100
|
|
24
|
+
Requires-Dist: uvicorn>=0.23
|
|
25
|
+
Requires-Dist: pydantic>=2
|
|
26
|
+
Requires-Dist: requests>=2.28
|
|
27
|
+
Requires-Dist: loguru>=0.7
|
|
28
|
+
Requires-Dist: segno>=1.6
|
|
29
|
+
Requires-Dist: anthropic>=0.69
|
|
30
|
+
Requires-Dist: tomli>=2; python_version < "3.11"
|
|
31
|
+
Requires-Dist: websockets>=12
|
|
32
|
+
Requires-Dist: pywinpty>=2; sys_platform == "win32"
|
|
33
|
+
Requires-Dist: pyte>=0.8
|
|
34
|
+
Provides-Extra: mssql
|
|
35
|
+
Requires-Dist: pyodbc>=4; extra == "mssql"
|
|
36
|
+
Provides-Extra: db
|
|
37
|
+
Requires-Dist: sqlalchemy>=2; extra == "db"
|
|
38
|
+
Provides-Extra: aws
|
|
39
|
+
Requires-Dist: boto3>=1.28; extra == "aws"
|
|
40
|
+
Provides-Extra: voice
|
|
41
|
+
Requires-Dist: faster-whisper>=1.0; extra == "voice"
|
|
42
|
+
Provides-Extra: desktop
|
|
43
|
+
Requires-Dist: pywebview>=4; extra == "desktop"
|
|
44
|
+
Provides-Extra: build
|
|
45
|
+
Requires-Dist: pyinstaller>=6; extra == "build"
|
|
46
|
+
Provides-Extra: dev
|
|
47
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
48
|
+
Requires-Dist: httpx>=0.24; extra == "dev"
|
|
49
|
+
Requires-Dist: openpyxl>=3; extra == "dev"
|
|
50
|
+
Dynamic: license-file
|
|
51
|
+
|
|
52
|
+
# Taskuary
|
|
53
|
+
|
|
54
|
+
[](https://github.com/ldbumble/taskuary/actions/workflows/ci.yml)
|
|
55
|
+
[](https://pypi.org/project/taskuary/)
|
|
56
|
+
[](https://github.com/ldbumble/taskuary)
|
|
57
|
+
[](LICENSE)
|
|
58
|
+
|
|
59
|
+
## Your inbox, staffed by AI agents.
|
|
60
|
+
|
|
61
|
+
Taskuary brings your mail, chats, issues, and scheduled reports onto one local timeline.
|
|
62
|
+
AI triage decides what is real work, your coding CLI works the tasks in your own repos,
|
|
63
|
+
and replies wait for your approval. Nothing sends or ships without you.
|
|
64
|
+
|
|
65
|
+

|
|
66
|
+
|
|
67
|
+
Taskuary is early—currently **v0.3.2**—and moving fast. The funnel, review queue,
|
|
68
|
+
agent sessions, and reports pipeline are in daily use; breaking changes are still possible
|
|
69
|
+
before 1.0.
|
|
70
|
+
|
|
71
|
+
## One timeline for incoming work
|
|
72
|
+
|
|
73
|
+
Outlook, Gmail, Teams, Slack, Telegram, WhatsApp, GitHub, Jira, alerts, and reports all
|
|
74
|
+
arrive on the same day-grouped rail. Each row says what it is and whether it needs you.
|
|
75
|
+
Open one to see the full message, its attachments, why triage ruled that way, the drafted
|
|
76
|
+
reply, and every available next step.
|
|
77
|
+
|
|
78
|
+

|
|
79
|
+
|
|
80
|
+
Coding work can go straight to Claude Code, Codex, Gemini, Cursor, Copilot, or any CLI that
|
|
81
|
+
accepts a prompt on stdin. You watch the live terminal, answer questions, review the diff,
|
|
82
|
+
and approve what happens next. Research, marketing, and other general work belong in a
|
|
83
|
+
conversational workspace built with assistant-ui and your existing Taskuary AI connections.
|
|
84
|
+
Assistant and terminal are two views of the same session: model, history, queued instructions,
|
|
85
|
+
image attachments, and the side-by-side browser stay put when you switch. The same workspace
|
|
86
|
+
also appears on the Wall.
|
|
87
|
+
|
|
88
|
+
## An assistant that notices what falls between tasks
|
|
89
|
+
|
|
90
|
+
The Assistant periodically reads what the hub can see and posts only when it has something
|
|
91
|
+
useful to say: a reply that went unanswered, a meeting that needs context, a task that went
|
|
92
|
+
quiet, or a pattern across the week's work. Every suggestion includes its evidence and can
|
|
93
|
+
be made into a task, dismissed, snoozed, or taught away with **Not this**.
|
|
94
|
+
|
|
95
|
+

|
|
96
|
+
|
|
97
|
+
It watches the systems you run on, not only the hub. Point it at a Sage Intacct query, a
|
|
98
|
+
database, a file, a REST or MCP tool, a cloud log group, or an agent skill—no saved report has
|
|
99
|
+
to stand behind any of them—and it reads each one silently on every check. That step would
|
|
100
|
+
otherwise ask you to know an object name and a list of field ids, so you can describe what it
|
|
101
|
+
should keep an eye on instead and the AI writes the source cards: it may only choose systems you
|
|
102
|
+
have actually connected, it reads the real schema before writing a query, and it asks rather
|
|
103
|
+
than guessing a filter. The same help sits on every source card in the report builder.
|
|
104
|
+
|
|
105
|
+
Its voice, schedule, model, and thresholds are yours to change. It leaves a note for its next
|
|
106
|
+
check, does not repeat itself, and stays silent when there is nothing worth interrupting you
|
|
107
|
+
for.
|
|
108
|
+
|
|
109
|
+
## Install
|
|
110
|
+
|
|
111
|
+
### Windows app
|
|
112
|
+
|
|
113
|
+
Download the latest single-file
|
|
114
|
+
[Taskuary.exe](https://github.com/ldbumble/taskuary/releases/latest/download/Taskuary.exe)
|
|
115
|
+
and open it. No Python or installer is required.
|
|
116
|
+
|
|
117
|
+
### Python
|
|
118
|
+
|
|
119
|
+
Python 3.10 or newer works on Windows, macOS, and Linux:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pip install taskuary
|
|
123
|
+
taskuary
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Taskuary opens at [http://127.0.0.1:7787](http://127.0.0.1:7787). For a native desktop
|
|
127
|
+
window instead, install `pip install "taskuary[desktop]"` and run `taskuary-desktop`.
|
|
128
|
+
|
|
129
|
+
### Docker
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
git clone https://github.com/ldbumble/taskuary
|
|
133
|
+
cd taskuary
|
|
134
|
+
docker compose up
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Then open [http://127.0.0.1:7787](http://127.0.0.1:7787). Docker runs the web app;
|
|
138
|
+
coding CLIs and the optional WhatsApp bridge remain on the host.
|
|
139
|
+
|
|
140
|
+
On first run, connect an AI provider or local Ollama model, add at least one inbound
|
|
141
|
+
channel, then choose the coding CLI that should receive tasks. The setup wizards test each
|
|
142
|
+
connection before it goes live.
|
|
143
|
+
|
|
144
|
+
## Installs
|
|
145
|
+
|
|
146
|
+

|
|
147
|
+
|
|
148
|
+
Redrawn every morning by [`downloads.yml`](.github/workflows/downloads.yml) from PyPI's own
|
|
149
|
+
numbers, with mirror traffic excluded — a full PyPI mirror pulls every release, so counting it
|
|
150
|
+
would let a handful of real users read as hundreds. What it does **not** claim is that CI
|
|
151
|
+
installs have been removed: the field that would separate `pip install` in a build runner from
|
|
152
|
+
one on somebody's laptop is in PyPI's BigQuery dataset, not in the public API. The daily series
|
|
153
|
+
is [docs/downloads.csv](docs/downloads.csv).
|
|
154
|
+
|
|
155
|
+
## Documentation
|
|
156
|
+
|
|
157
|
+
- [Getting started](docs/getting-started.md)—installation, first-run setup, Docker, and data
|
|
158
|
+
- [Product guide](docs/product-guide.md)—the workflow, learning loop, agents, and operator documents
|
|
159
|
+
- [Integrations](docs/integrations.md)—channels, AI providers, work systems, and report sources
|
|
160
|
+
- [Reports and the Assistant](docs/reports-and-assistant.md)—the report pipeline, letting the AI write the source cards, and what the Assistant watches
|
|
161
|
+
- [Status and roadmap](docs/roadmap.md)—what works today and what is next
|
|
162
|
+
- [Contributing](CONTRIBUTING.md)—development setup and contribution guide
|
|
163
|
+
|
|
164
|
+
Taskuary is free and open source under the [MIT License](LICENSE). Issues and pull requests
|
|
165
|
+
are welcome; security reports belong in [SECURITY.md](SECURITY.md).
|
taskuary-0.3.2/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Taskuary
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ldbumble/taskuary/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/taskuary/)
|
|
5
|
+
[](https://github.com/ldbumble/taskuary)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
## Your inbox, staffed by AI agents.
|
|
9
|
+
|
|
10
|
+
Taskuary brings your mail, chats, issues, and scheduled reports onto one local timeline.
|
|
11
|
+
AI triage decides what is real work, your coding CLI works the tasks in your own repos,
|
|
12
|
+
and replies wait for your approval. Nothing sends or ships without you.
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
Taskuary is early—currently **v0.3.2**—and moving fast. The funnel, review queue,
|
|
17
|
+
agent sessions, and reports pipeline are in daily use; breaking changes are still possible
|
|
18
|
+
before 1.0.
|
|
19
|
+
|
|
20
|
+
## One timeline for incoming work
|
|
21
|
+
|
|
22
|
+
Outlook, Gmail, Teams, Slack, Telegram, WhatsApp, GitHub, Jira, alerts, and reports all
|
|
23
|
+
arrive on the same day-grouped rail. Each row says what it is and whether it needs you.
|
|
24
|
+
Open one to see the full message, its attachments, why triage ruled that way, the drafted
|
|
25
|
+
reply, and every available next step.
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
Coding work can go straight to Claude Code, Codex, Gemini, Cursor, Copilot, or any CLI that
|
|
30
|
+
accepts a prompt on stdin. You watch the live terminal, answer questions, review the diff,
|
|
31
|
+
and approve what happens next. Research, marketing, and other general work belong in a
|
|
32
|
+
conversational workspace built with assistant-ui and your existing Taskuary AI connections.
|
|
33
|
+
Assistant and terminal are two views of the same session: model, history, queued instructions,
|
|
34
|
+
image attachments, and the side-by-side browser stay put when you switch. The same workspace
|
|
35
|
+
also appears on the Wall.
|
|
36
|
+
|
|
37
|
+
## An assistant that notices what falls between tasks
|
|
38
|
+
|
|
39
|
+
The Assistant periodically reads what the hub can see and posts only when it has something
|
|
40
|
+
useful to say: a reply that went unanswered, a meeting that needs context, a task that went
|
|
41
|
+
quiet, or a pattern across the week's work. Every suggestion includes its evidence and can
|
|
42
|
+
be made into a task, dismissed, snoozed, or taught away with **Not this**.
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
It watches the systems you run on, not only the hub. Point it at a Sage Intacct query, a
|
|
47
|
+
database, a file, a REST or MCP tool, a cloud log group, or an agent skill—no saved report has
|
|
48
|
+
to stand behind any of them—and it reads each one silently on every check. That step would
|
|
49
|
+
otherwise ask you to know an object name and a list of field ids, so you can describe what it
|
|
50
|
+
should keep an eye on instead and the AI writes the source cards: it may only choose systems you
|
|
51
|
+
have actually connected, it reads the real schema before writing a query, and it asks rather
|
|
52
|
+
than guessing a filter. The same help sits on every source card in the report builder.
|
|
53
|
+
|
|
54
|
+
Its voice, schedule, model, and thresholds are yours to change. It leaves a note for its next
|
|
55
|
+
check, does not repeat itself, and stays silent when there is nothing worth interrupting you
|
|
56
|
+
for.
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
### Windows app
|
|
61
|
+
|
|
62
|
+
Download the latest single-file
|
|
63
|
+
[Taskuary.exe](https://github.com/ldbumble/taskuary/releases/latest/download/Taskuary.exe)
|
|
64
|
+
and open it. No Python or installer is required.
|
|
65
|
+
|
|
66
|
+
### Python
|
|
67
|
+
|
|
68
|
+
Python 3.10 or newer works on Windows, macOS, and Linux:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install taskuary
|
|
72
|
+
taskuary
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Taskuary opens at [http://127.0.0.1:7787](http://127.0.0.1:7787). For a native desktop
|
|
76
|
+
window instead, install `pip install "taskuary[desktop]"` and run `taskuary-desktop`.
|
|
77
|
+
|
|
78
|
+
### Docker
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
git clone https://github.com/ldbumble/taskuary
|
|
82
|
+
cd taskuary
|
|
83
|
+
docker compose up
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then open [http://127.0.0.1:7787](http://127.0.0.1:7787). Docker runs the web app;
|
|
87
|
+
coding CLIs and the optional WhatsApp bridge remain on the host.
|
|
88
|
+
|
|
89
|
+
On first run, connect an AI provider or local Ollama model, add at least one inbound
|
|
90
|
+
channel, then choose the coding CLI that should receive tasks. The setup wizards test each
|
|
91
|
+
connection before it goes live.
|
|
92
|
+
|
|
93
|
+
## Installs
|
|
94
|
+
|
|
95
|
+

|
|
96
|
+
|
|
97
|
+
Redrawn every morning by [`downloads.yml`](.github/workflows/downloads.yml) from PyPI's own
|
|
98
|
+
numbers, with mirror traffic excluded — a full PyPI mirror pulls every release, so counting it
|
|
99
|
+
would let a handful of real users read as hundreds. What it does **not** claim is that CI
|
|
100
|
+
installs have been removed: the field that would separate `pip install` in a build runner from
|
|
101
|
+
one on somebody's laptop is in PyPI's BigQuery dataset, not in the public API. The daily series
|
|
102
|
+
is [docs/downloads.csv](docs/downloads.csv).
|
|
103
|
+
|
|
104
|
+
## Documentation
|
|
105
|
+
|
|
106
|
+
- [Getting started](docs/getting-started.md)—installation, first-run setup, Docker, and data
|
|
107
|
+
- [Product guide](docs/product-guide.md)—the workflow, learning loop, agents, and operator documents
|
|
108
|
+
- [Integrations](docs/integrations.md)—channels, AI providers, work systems, and report sources
|
|
109
|
+
- [Reports and the Assistant](docs/reports-and-assistant.md)—the report pipeline, letting the AI write the source cards, and what the Assistant watches
|
|
110
|
+
- [Status and roadmap](docs/roadmap.md)—what works today and what is next
|
|
111
|
+
- [Contributing](CONTRIBUTING.md)—development setup and contribution guide
|
|
112
|
+
|
|
113
|
+
Taskuary is free and open source under the [MIT License](LICENSE). Issues and pull requests
|
|
114
|
+
are welcome; security reports belong in [SECURITY.md](SECURITY.md).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "taskuary"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Automate your job - the local-first operating system for your work: AI triage in, coding agents out."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -27,6 +27,7 @@ dependencies = [
|
|
|
27
27
|
"pydantic>=2",
|
|
28
28
|
"requests>=2.28",
|
|
29
29
|
"loguru>=0.7",
|
|
30
|
+
"segno>=1.6", # WhatsApp pairing QR drawn ON the card - pure Python, no Pillow
|
|
30
31
|
"anthropic>=0.69",
|
|
31
32
|
"tomli>=2; python_version<'3.11'",
|
|
32
33
|
"websockets>=12", # uvicorn's ws transport - the terminal streams over it
|
|
@@ -38,6 +39,7 @@ dependencies = [
|
|
|
38
39
|
mssql = ["pyodbc>=4"]
|
|
39
40
|
db = ["sqlalchemy>=2"] # 'any database' URLs; add the engine driver too (psycopg2-binary, pymysql, ...)
|
|
40
41
|
aws = ["boto3>=1.28"] # azure needs no extra: tokens ride plain requests
|
|
42
|
+
voice = ["faster-whisper>=1.0"] # Local Whisper: speech to text on this machine, no key, audio never leaves the box
|
|
41
43
|
desktop = ["pywebview>=4"]
|
|
42
44
|
build = ["pyinstaller>=6"]
|
|
43
45
|
dev = ["pytest>=7", "httpx>=0.24", "openpyxl>=3"] # openpyxl READS xlsx in tests; the writer is ours
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Taskuary -
|
|
1
|
+
"""Taskuary - your work AI assistant: AI triage in, coding agents out, you in charge."""
|
|
2
2
|
# ONE place holds the version and it is pyproject.toml, because that is the one the git tag has
|
|
3
3
|
# to match (RELEASING.md). A second copy here said 0.2.0 through the whole of 0.2.1: the CLI
|
|
4
4
|
# banner, the API and the header pill all reported it, and the header's own tooltip told the
|
|
@@ -6,6 +6,7 @@ run so code changes are first-class, every run traced + audited.
|
|
|
6
6
|
"""
|
|
7
7
|
import json, os, re, shutil, subprocess, threading, time
|
|
8
8
|
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
9
10
|
from loguru import logger
|
|
10
11
|
|
|
11
12
|
from .store import task_ref
|
|
@@ -75,26 +76,76 @@ def _shim_target(path: str) -> list:
|
|
|
75
76
|
return []
|
|
76
77
|
|
|
77
78
|
|
|
79
|
+
def _programs_copy(base: str) -> str:
|
|
80
|
+
"""The ordinary per-user install of a CLI that is ALSO published as a Store app - codex ships
|
|
81
|
+
both. which() returns whichever comes first on PATH, and on some machines that is a stub this
|
|
82
|
+
account may not execute at all."""
|
|
83
|
+
root = os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs')
|
|
84
|
+
want = base.lower()
|
|
85
|
+
for dirpath, dirs, files in os.walk(root): # a missing root simply yields nothing
|
|
86
|
+
for f in files:
|
|
87
|
+
if f.lower() == want: return os.path.join(dirpath, f)
|
|
88
|
+
if dirpath.count(os.sep) - root.count(os.sep) >= 3: dirs.clear() # vendor/app/bin is deep enough
|
|
89
|
+
return ''
|
|
90
|
+
|
|
91
|
+
|
|
78
92
|
def _resolve_cmd(name: str) -> list:
|
|
79
93
|
"""Windows can't CreateProcess a bare 'claude': npm installs it as claude.cmd, which only
|
|
80
94
|
PATH-resolves via which(). Reaching THROUGH the shim beats running it under cmd /c - see
|
|
81
95
|
_shim_target for why that difference decides whether a prompt arrives whole."""
|
|
82
96
|
path = shutil.which(name) or shutil.which(name, path=_fresh_path())
|
|
97
|
+
if not path and re.search(r'[\\/]', str(name)):
|
|
98
|
+
# A saved ABSOLUTE path that has moved. codex installs itself into
|
|
99
|
+
# ...\Codex\bin\<version hash>\codex.exe, so a profile pinned to one of those breaks on
|
|
100
|
+
# the next update and reads as "the CLI is gone". The NAME is the durable half: ask PATH
|
|
101
|
+
# again for it, which is what the owner would have done by hand.
|
|
102
|
+
base = re.split(r'[\\/]', str(name))[-1]
|
|
103
|
+
path = shutil.which(base) or shutil.which(base, path=_fresh_path())
|
|
104
|
+
if path: logger.info(f'{name} has moved; using {path} instead')
|
|
83
105
|
if not path:
|
|
84
106
|
raise FileNotFoundError(f"'{name}' not found on PATH - is the CLI installed?")
|
|
85
107
|
if os.name == 'nt' and path.lower().endswith(('.cmd', '.bat')):
|
|
86
108
|
return _shim_target(path) or ['cmd', '/c', path]
|
|
87
|
-
if os.name == 'nt' and '\\windowsapps\\' in path.lower()
|
|
109
|
+
if os.name == 'nt' and '\\windowsapps\\' in path.lower():
|
|
88
110
|
# which() walked into C:\Program Files\WindowsApps\<package>\...\codex.EXE - a Store package
|
|
89
111
|
# folder, which CreateProcess is refused ([WinError 5] Access is denied). The runnable
|
|
90
112
|
# thing is the execution ALIAS in the user's own WindowsApps folder; fall back to the
|
|
91
113
|
# shell, which resolves aliases the way a typed command does.
|
|
92
114
|
import ntpath # a Windows path, split as one wherever this runs (CI is Linux and macOS)
|
|
93
|
-
|
|
115
|
+
base = ntpath.basename(path)
|
|
116
|
+
# An ordinary install beats either stub: the alias itself answers "Access is denied." when
|
|
117
|
+
# the package is not registered for this account, and cmd only forwards that refusal.
|
|
118
|
+
real = _programs_copy(base)
|
|
119
|
+
if real: return [real]
|
|
120
|
+
if '\\microsoft\\windowsapps\\' in path.lower(): return [path] # the alias, and nothing better
|
|
121
|
+
alias = os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Microsoft', 'WindowsApps', base)
|
|
94
122
|
return [alias] if os.path.exists(alias) else ['cmd', '/c', name]
|
|
95
123
|
return [path]
|
|
96
124
|
|
|
97
125
|
|
|
126
|
+
def child_env() -> dict:
|
|
127
|
+
"""The environment a CLI is entitled to expect.
|
|
128
|
+
|
|
129
|
+
codex refuses to start without one: `Error finding codex home: Could not find home
|
|
130
|
+
directory` (an owner's machine, 2026-08-31), and it is not a codex bug - a Taskuary
|
|
131
|
+
launched from a service, a scheduled task, or a shortcut with a scrubbed environment does
|
|
132
|
+
not always pass USERPROFILE down, and the Rust `dirs` crate has nothing else to go on.
|
|
133
|
+
Python can answer the question, so it answers it instead of letting the CLI guess: HOME
|
|
134
|
+
and USERPROFILE where they are missing, and CODEX_HOME - the variable codex checks FIRST,
|
|
135
|
+
before it ever asks the OS - pointed at the same place its own installer would use.
|
|
136
|
+
"""
|
|
137
|
+
env = dict(os.environ)
|
|
138
|
+
try: home = str(Path.home())
|
|
139
|
+
except (RuntimeError, OSError): return env # nothing better to say than nothing
|
|
140
|
+
env.setdefault('HOME', home)
|
|
141
|
+
if os.name == 'nt':
|
|
142
|
+
env.setdefault('USERPROFILE', home)
|
|
143
|
+
drive, tail = os.path.splitdrive(home)
|
|
144
|
+
if drive: env.setdefault('HOMEDRIVE', drive); env.setdefault('HOMEPATH', tail)
|
|
145
|
+
env.setdefault('CODEX_HOME', os.path.join(home, '.codex'))
|
|
146
|
+
return env
|
|
147
|
+
|
|
148
|
+
|
|
98
149
|
def _fmt_input(inp) -> str:
|
|
99
150
|
"""The one field a human wants to see per tool call - command, path, pattern…"""
|
|
100
151
|
if not isinstance(inp, dict): return str(inp)[:140]
|
|
@@ -146,16 +197,98 @@ def signed_out_msg(name: str, why: str) -> str:
|
|
|
146
197
|
return f"{name} is signed out on this machine ({why.strip()[:160]}). Open a terminal, {how}, then come back here and try again."
|
|
147
198
|
|
|
148
199
|
|
|
149
|
-
|
|
200
|
+
# Windows refuses to START some installs rather than failing inside them, and says only
|
|
201
|
+
# "Access is denied." - which reads as an account or billing problem with the AI provider and is
|
|
202
|
+
# nothing of the kind: the CLI never ran. Reported on another owner's machine (2026-08-31) as
|
|
203
|
+
# `codex exit 1: Access is denied.` with no other output.
|
|
204
|
+
_DENIED = re.compile(r'access is denied|winerror 5|permission denied|operation not permitted', re.I)
|
|
205
|
+
_NO_HOME = re.compile(r'could not find home directory|finding codex home|HOME.{0,20}not set', re.I)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def no_home_msg(name: str) -> str:
|
|
209
|
+
return (f'{name} could not find a home directory to keep its own settings and sign-in in. '
|
|
210
|
+
'Taskuary now hands every CLI a HOME, USERPROFILE and CODEX_HOME, so if this persists '
|
|
211
|
+
'the account running Taskuary has no profile directory at all - which happens when it '
|
|
212
|
+
'runs as a Windows service or a scheduled task under SYSTEM or a managed account. Run '
|
|
213
|
+
f'Taskuary as the same user who runs `{name}` in a terminal, or set CODEX_HOME '
|
|
214
|
+
'explicitly for that account, then sign in once with `codex login`.')
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def denied_msg(name: str, path: str, why: str) -> str:
|
|
218
|
+
where = f' ({path})' if path else ''
|
|
219
|
+
return (f'Windows would not start {name}{where}: "{str(why).strip()[:120]}". The CLI never ran, so '
|
|
220
|
+
f'this is not a sign-in or billing problem. Usual causes, in order: {name} came from the '
|
|
221
|
+
'Microsoft Store and its app-execution alias does not work for this account (install the '
|
|
222
|
+
'ordinary build instead, or reinstall it for this user); antivirus or an AppLocker policy is '
|
|
223
|
+
'blocking the executable; the folder the agent works in is not readable by this account. '
|
|
224
|
+
f'`where {name}` shows which copy is being found - running that exact path by hand '
|
|
225
|
+
'reproduces it in one line.')
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _cli_name(cmd: str) -> str:
|
|
229
|
+
"""Executable name for either a bare command or a Windows/POSIX path."""
|
|
230
|
+
return re.split(r'[\\/]', str(cmd or ''))[-1].lower().rsplit('.', 1)[0]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _codex_tool(item: dict):
|
|
234
|
+
"""Turn a Codex JSONL item into the common visual tool name/input contract."""
|
|
235
|
+
typ = item.get('type')
|
|
236
|
+
if typ == 'command_execution': return 'shell', {'command': item.get('command') or ''}
|
|
237
|
+
if typ == 'file_change': return 'file change', {'changes': item.get('changes') or []}
|
|
238
|
+
if typ == 'mcp_tool_call':
|
|
239
|
+
return item.get('tool') or item.get('name') or 'MCP tool', item.get('arguments') or item.get('args') or {}
|
|
240
|
+
if typ == 'web_search': return 'web search', {'query': item.get('query') or ''}
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def runs_here(profile: dict) -> bool:
|
|
245
|
+
"""Does this profile's command resolve to something we can start?"""
|
|
246
|
+
try: return bool(_resolve_cmd((profile or {}).get('cmd') or 'claude'))
|
|
247
|
+
except (FileNotFoundError, OSError): return False
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def profiles(store) -> dict:
|
|
251
|
+
out = {}
|
|
252
|
+
for a in store.list_agents():
|
|
253
|
+
try: out[a['Name']] = json.loads(a.get('Config') or '{}')
|
|
254
|
+
except ValueError: out[a['Name']] = {}
|
|
255
|
+
return out
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def default_agent(store) -> str:
|
|
259
|
+
"""Which agent a task goes to when nobody picked one.
|
|
260
|
+
|
|
261
|
+
The owner's choice wins - unless its CLI is not on this machine, in which case one that IS
|
|
262
|
+
beats a name that can only fail. Taskuary ships `coder` = claude, so on a machine with only
|
|
263
|
+
codex installed every dispatch died on a CLI nobody had, and the Board showed it as the
|
|
264
|
+
agent's failure (an owner's machine, 2026-08-31)."""
|
|
265
|
+
want = str(store.get_settings().get('default_agent') or 'coder').strip()
|
|
266
|
+
profs = profiles(store)
|
|
267
|
+
if want not in profs or runs_here(profs[want]): return want
|
|
268
|
+
return next((n for n, prof in profs.items() if runs_here(prof)), want)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def run_cli(profile: dict, prompt: str, trace, resume: str = None, cancel=None):
|
|
150
272
|
"""One headless invocation of the configured CLI, output STREAMED line by line into
|
|
151
273
|
the run trace so the Board shows the agent working live. claude's stream-json events
|
|
152
274
|
render as readable tool/text lines; any other CLI's plain stdout streams as-is.
|
|
153
275
|
Returns (result, session_id, diff)."""
|
|
154
276
|
name = profile.get('cmd', 'claude')
|
|
155
|
-
|
|
277
|
+
args = list(profile.get('args') or preset_args(name) or ['-p'])
|
|
278
|
+
# Codex's normal exec output is human prose with no boundary between commands, searches,
|
|
279
|
+
# edits and the final answer. JSONL is an exec-only presentation flag, so add it here (not
|
|
280
|
+
# to the saved profile, which is also used to open the interactive terminal TUI).
|
|
281
|
+
is_codex = _cli_name(name) == 'codex' and bool(args) and args[0] in ('exec', 'e')
|
|
282
|
+
if is_codex and '--json' not in args: args.append('--json')
|
|
283
|
+
cmd = _resolve_cmd(name) + args
|
|
156
284
|
# which model works it: profile default, or a per-run override from the UI. The flag
|
|
157
285
|
# name is configurable because every CLI spells it differently (claude/codex: --model).
|
|
158
|
-
if profile.get('model'):
|
|
286
|
+
if profile.get('model'):
|
|
287
|
+
# 'gpt-5.4@high' spells a codex model and its reasoning level in one pick (climodels)
|
|
288
|
+
from .climodels import split_pick
|
|
289
|
+
m, eff = split_pick(profile['model'])
|
|
290
|
+
cmd += [profile.get('model_arg') or '--model', m]
|
|
291
|
+
if eff: cmd += ['-c', f'model_reasoning_effort={eff}']
|
|
159
292
|
if resume and profile.get('resume_args'): cmd += list(profile['resume_args']) + [resume]
|
|
160
293
|
cwd = profile.get('cwd')
|
|
161
294
|
head0 = _git(cwd, 'rev-parse', 'HEAD')
|
|
@@ -163,22 +296,34 @@ def run_cli(profile: dict, prompt: str, trace, resume: str = None):
|
|
|
163
296
|
trace('tool', 'cli', f'{name} cwd={cwd or os.getcwd()}' + (f' resume={resume}' if resume else ''))
|
|
164
297
|
try:
|
|
165
298
|
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
166
|
-
text=True, encoding='utf-8', errors='replace', cwd=cwd, shell=False
|
|
299
|
+
text=True, encoding='utf-8', errors='replace', cwd=cwd, shell=False,
|
|
300
|
+
env=child_env())
|
|
167
301
|
except PermissionError as e:
|
|
168
|
-
#
|
|
169
|
-
|
|
302
|
+
# which() found something that cannot be executed from here. "Not installed" sent people
|
|
303
|
+
# off to reinstall a CLI that was already there; the reason is in denied_msg.
|
|
304
|
+
raise FileNotFoundError(denied_msg(name, cmd[0] if cmd else '', e)) from e
|
|
170
305
|
timed = threading.Event()
|
|
171
306
|
killer = threading.Timer(profile.get('timeout', 1200), lambda: (timed.set(), p.kill()))
|
|
172
307
|
killer.start()
|
|
308
|
+
# A browser Cancel closes the streaming response. Kill the CLI too; otherwise the UI says
|
|
309
|
+
# stopped while an invisible agent keeps using tools in the background.
|
|
310
|
+
if cancel is not None:
|
|
311
|
+
def _cancel():
|
|
312
|
+
cancel.wait()
|
|
313
|
+
if cancel.is_set() and p.poll() is None:
|
|
314
|
+
try: p.kill()
|
|
315
|
+
except Exception: pass
|
|
316
|
+
threading.Thread(target=_cancel, daemon=True).start()
|
|
173
317
|
err_buf = []
|
|
174
|
-
threading.Thread(target=lambda: err_buf.append(p.stderr.read()), daemon=True)
|
|
318
|
+
err_t = threading.Thread(target=lambda: err_buf.append(p.stderr.read()), daemon=True)
|
|
319
|
+
err_t.start()
|
|
175
320
|
# stdin feed on its own thread: writing a big prompt while the child is already
|
|
176
321
|
# emitting output can deadlock both pipes otherwise
|
|
177
322
|
def _feed():
|
|
178
323
|
try: p.stdin.write(prompt); p.stdin.close()
|
|
179
324
|
except Exception: pass
|
|
180
325
|
threading.Thread(target=_feed, daemon=True).start()
|
|
181
|
-
raw, final = [], None
|
|
326
|
+
raw, final, streamed_out, streamed_sid, open_tools = [], None, '', None, set()
|
|
182
327
|
try:
|
|
183
328
|
for line in p.stdout:
|
|
184
329
|
line = line.rstrip('\n')
|
|
@@ -188,17 +333,57 @@ def run_cli(profile: dict, prompt: str, trace, resume: str = None):
|
|
|
188
333
|
except ValueError: trace('live', name, line[:400]); continue
|
|
189
334
|
if isinstance(j, dict) and (j.get('type') == 'result' or ('result' in j and 'type' not in j)):
|
|
190
335
|
final = j; continue
|
|
336
|
+
# Preserve the CLI's structured work for visual clients. The existing readable
|
|
337
|
+
# `live` line remains for Board traces and the terminal renderer.
|
|
338
|
+
if isinstance(j, dict) and j.get('type') == 'assistant':
|
|
339
|
+
for c in (j.get('message') or {}).get('content') or []:
|
|
340
|
+
if c.get('type') == 'tool_use':
|
|
341
|
+
trace('tool_call', c.get('name') or 'tool', {
|
|
342
|
+
'tool_call_id': c.get('id') or f'tool-{len(raw)}', 'args': c.get('input') or {}})
|
|
343
|
+
elif c.get('type') == 'text' and str(c.get('text') or '').strip():
|
|
344
|
+
trace('progress', 'text', str(c['text']).strip())
|
|
345
|
+
elif isinstance(j, dict) and j.get('type') == 'user':
|
|
346
|
+
for c in (j.get('message') or {}).get('content') or []:
|
|
347
|
+
if isinstance(c, dict) and c.get('type') == 'tool_result':
|
|
348
|
+
trace('tool_result', c.get('tool_use_id') or 'tool', {
|
|
349
|
+
'result': _result_text(c), 'is_error': bool(c.get('is_error'))})
|
|
350
|
+
# Codex `exec --json` speaks item lifecycle events instead of Claude content blocks.
|
|
351
|
+
# Normalize both into one stream so assistant-ui does not care which CLI is logged in.
|
|
352
|
+
if isinstance(j, dict) and j.get('type') == 'thread.started':
|
|
353
|
+
streamed_sid = j.get('thread_id') or streamed_sid
|
|
354
|
+
if isinstance(j, dict) and j.get('type') in ('item.started', 'item.updated', 'item.completed'):
|
|
355
|
+
item = j.get('item') or {}
|
|
356
|
+
iid = item.get('id') or f"item-{len(raw)}"
|
|
357
|
+
tool = _codex_tool(item)
|
|
358
|
+
if tool and iid not in open_tools:
|
|
359
|
+
trace('tool_call', tool[0], {'tool_call_id': iid, 'args': tool[1]})
|
|
360
|
+
open_tools.add(iid)
|
|
361
|
+
if tool and j.get('type') == 'item.completed':
|
|
362
|
+
result = item.get('aggregated_output') or item.get('output') or item.get('status') or ''
|
|
363
|
+
failed = item.get('status') == 'failed' or item.get('exit_code') not in (None, 0)
|
|
364
|
+
trace('tool_result', iid, {'result': str(result), 'is_error': failed})
|
|
365
|
+
if item.get('type') in ('agent_message', 'reasoning') and str(item.get('text') or '').strip():
|
|
366
|
+
text = str(item['text']).strip()
|
|
367
|
+
trace('progress', item.get('type'), text)
|
|
368
|
+
if item.get('type') == 'agent_message': streamed_out = text
|
|
191
369
|
shown = _live_line(j) if isinstance(j, dict) else None
|
|
192
370
|
if shown: trace('live', name, shown)
|
|
193
371
|
p.wait()
|
|
194
372
|
finally:
|
|
195
373
|
killer.cancel()
|
|
374
|
+
err_t.join(5) # the exit code can land before the stderr reader has appended - 'boom' read as 'no output' on a fast CI box
|
|
196
375
|
if p.returncode != 0:
|
|
376
|
+
if cancel is not None and cancel.is_set(): raise RuntimeError('cancelled')
|
|
197
377
|
why = f'timed out after {profile.get("timeout", 1200)}s' if timed.is_set() else \
|
|
198
378
|
((err_buf[0] if err_buf else '') or '\n'.join(raw[-5:]) or 'no output')[:500]
|
|
199
379
|
if _SIGNED_OUT.search(why): raise RuntimeError(signed_out_msg(name, why))
|
|
380
|
+
# a refusal to START, not a failed run: the CLI produced no output of its own and the
|
|
381
|
+
# only thing on stderr is the refusal
|
|
382
|
+
if _DENIED.search(why) and not raw: raise RuntimeError(denied_msg(name, cmd[0] if cmd else '', why))
|
|
383
|
+
if _NO_HOME.search(why): raise RuntimeError(no_home_msg(_cli_name(name) or name))
|
|
200
384
|
raise RuntimeError(f'{name} exit {p.returncode}: {why}')
|
|
201
385
|
if final is not None: out, sid = str(final.get('result') or '').strip(), final.get('session_id')
|
|
386
|
+
elif streamed_out: out, sid = streamed_out, streamed_sid
|
|
202
387
|
else: out, sid = parse_cli_json('\n'.join(raw))
|
|
203
388
|
trace('output', name, out[-1000:])
|
|
204
389
|
diff = ''
|
|
@@ -222,6 +407,9 @@ def task_context(store, task_id: int) -> str:
|
|
|
222
407
|
lines += ['', 'Thread:'] + [f"- {c.get('Actor')}: {str(c.get('Body'))[:300]}" for c in d['comments']]
|
|
223
408
|
mem = memory_block(store, d['messages'])
|
|
224
409
|
if mem: lines += ['', mem]
|
|
410
|
+
from . import knowledge
|
|
411
|
+
kb = knowledge.block(store, ' '.join(f"{m.get('Subject') or ''} {m.get('BodyText') or ''}" for m in d['messages'])[:4000])
|
|
412
|
+
if kb: lines += [kb.strip()]
|
|
225
413
|
return '\n'.join(lines)
|
|
226
414
|
|
|
227
415
|
|
|
@@ -26,7 +26,7 @@ def live_for(store, cid: int):
|
|
|
26
26
|
"""The live setup session for this card, if one is open - the button reattaches instead of
|
|
27
27
|
starting a second agent on the same form."""
|
|
28
28
|
from . import terminal as term
|
|
29
|
-
for t in term.SESSIONS.values():
|
|
29
|
+
for t in list(term.SESSIONS.values()):
|
|
30
30
|
if not (t.alive and t.task_id): continue
|
|
31
31
|
tk = store.get_task(t.task_id) or {}
|
|
32
32
|
if tk.get('Kind') == KIND and tag(cid) in str(tk.get('Tags') or ''): return {**t.info(), 'taskId': t.task_id}
|
|
@@ -85,7 +85,8 @@ def start(store, server: dict, cid: int, guide: list, fields: list = None, secre
|
|
|
85
85
|
if not c: raise ValueError('connector not found')
|
|
86
86
|
live = live_for(store, cid)
|
|
87
87
|
if live: return {**live, 'existing': True}
|
|
88
|
-
|
|
88
|
+
from . import agents as hub_agents
|
|
89
|
+
agent = (agent or hub_agents.default_agent(store)).strip()
|
|
89
90
|
if not store.get_agent(agent): raise ValueError(f'no CLI agent named {agent!r} - add one under Connectors > AI CLI agents first')
|
|
90
91
|
tid = store.create_task({'Title': f"Set up {c.get('Name') or c.get('Type')}", 'Kind': KIND, 'Status': 'in_progress', 'Tags': tag(cid),
|
|
91
92
|
'Summary': f"{agent} walks the owner through the {c.get('Type')} guide in a live session on the card and saves what they give it."}, actor)
|