pal-code 0.1.1__tar.gz → 0.1.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.
Files changed (296) hide show
  1. {pal_code-0.1.1 → pal_code-0.1.2}/LICENSE +1 -1
  2. {pal_code-0.1.1 → pal_code-0.1.2}/PKG-INFO +2 -2
  3. {pal_code-0.1.1 → pal_code-0.1.2}/pyproject.toml +2 -2
  4. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/__init__.py +1 -1
  5. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/cli.py +5 -2
  6. pal_code-0.1.2/src/pal/context_window.py +42 -0
  7. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/studio/routes.py +2 -2
  8. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/studio/runs.py +21 -2
  9. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/bench.py +8 -1
  10. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/cli.py +111 -20
  11. pal_code-0.1.2/src/pal/terminal/logo.py +140 -0
  12. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/session.py +13 -0
  13. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/setup.py +104 -3
  14. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/ui.py +53 -29
  15. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/web/app.py +6 -0
  16. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/web/static/studio.css +2 -0
  17. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/web/templates/index.html +14 -6
  18. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/web/templates/studio.html +2 -2
  19. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal_code.egg-info/PKG-INFO +2 -2
  20. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal_code.egg-info/SOURCES.txt +1 -0
  21. pal_code-0.1.1/src/pal/terminal/logo.py +0 -117
  22. {pal_code-0.1.1 → pal_code-0.1.2}/README.md +0 -0
  23. {pal_code-0.1.1 → pal_code-0.1.2}/setup.cfg +0 -0
  24. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/__main__.py +0 -0
  25. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/access_manager/__init__.py +0 -0
  26. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/access_manager/components/__init__.py +0 -0
  27. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/access_manager/components/access_control.py +0 -0
  28. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/access_manager/components/base.py +0 -0
  29. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/access_manager/components/intervention.py +0 -0
  30. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/access_manager/components/privilege_registry.py +0 -0
  31. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/access_manager/pipeline.py +0 -0
  32. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/access_manager/utils/__init__.py +0 -0
  33. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/agent_constraints/__init__.py +0 -0
  34. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/agent_constraints/components/AGENT_OUTPUT.md +0 -0
  35. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/agent_constraints/components/__init__.py +0 -0
  36. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/agent_constraints/components/constraints.py +0 -0
  37. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/agent_constraints/pipeline.py +0 -0
  38. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/agent_constraints/utils/__init__.py +0 -0
  39. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/agents/__init__.py +0 -0
  40. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/agents/coding.py +0 -0
  41. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/agents/docs.py +0 -0
  42. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/chat/__init__.py +0 -0
  43. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/chat/demo.py +0 -0
  44. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/chat/providers.py +0 -0
  45. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/chat/service.py +0 -0
  46. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/chat/services/__init__.py +0 -0
  47. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/chat/services/base.py +0 -0
  48. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/chat/services/data.py +0 -0
  49. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/chat/services/media.py +0 -0
  50. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/chat/services/web.py +0 -0
  51. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/chat/sessions.py +0 -0
  52. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/chat/tools.py +0 -0
  53. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/chat/uploads.py +0 -0
  54. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/compression/__init__.py +0 -0
  55. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/compression/components/__init__.py +0 -0
  56. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/compression/components/adverbs.py +0 -0
  57. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/compression/components/hedges.py +0 -0
  58. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/compression/components/interjections.py +0 -0
  59. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/compression/components/minifier.py +0 -0
  60. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/compression/components/output_compressor.py +0 -0
  61. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/compression/components/pleonasms.py +0 -0
  62. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/compression/components/punctuations.py +0 -0
  63. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/compression/components/stopwords.py +0 -0
  64. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/compression/history.py +0 -0
  65. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/compression/pipeline.py +0 -0
  66. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/compression/utils/__init__.py +0 -0
  67. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/compression/utils/negations.py +0 -0
  68. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/compression/utils/nltk_setup.py +0 -0
  69. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/compression/utils/pronouns.py +0 -0
  70. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/compression/utils/proselint_setup.py +0 -0
  71. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/compression/utils/spacy_setup.py +0 -0
  72. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/config/default.yaml +0 -0
  73. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/config.py +0 -0
  74. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/context_manager/__init__.py +0 -0
  75. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/context_manager/chat_context.py +0 -0
  76. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/context_manager/components/__init__.py +0 -0
  77. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/context_manager/components/assembler.py +0 -0
  78. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/context_manager/components/context_cache.py +0 -0
  79. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/context_manager/components/kv_bridge.py +0 -0
  80. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/context_manager/components/pager.py +0 -0
  81. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/context_manager/components/simple_context_manager.py +0 -0
  82. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/context_manager/components/turns.py +0 -0
  83. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/context_manager/pipeline.py +0 -0
  84. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/context_manager/utils/__init__.py +0 -0
  85. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/context_manager/utils/hashing.py +0 -0
  86. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/context_manager/utils/relevance.py +0 -0
  87. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/context_manager/utils/storage.py +0 -0
  88. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/documents/__init__.py +0 -0
  89. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/documents/components/__init__.py +0 -0
  90. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/documents/components/chunker.py +0 -0
  91. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/documents/components/index.py +0 -0
  92. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/documents/utils/__init__.py +0 -0
  93. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/errors.py +0 -0
  94. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/input_converters/__init__.py +0 -0
  95. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/input_converters/components/__init__.py +0 -0
  96. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/input_converters/components/base.py +0 -0
  97. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/input_converters/components/pandoc_converter.py +0 -0
  98. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/input_converters/components/pdf_converter.py +0 -0
  99. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/input_converters/components/pptx_converter.py +0 -0
  100. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/input_converters/components/xlsx_converter.py +0 -0
  101. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/input_converters/pipeline.py +0 -0
  102. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/input_converters/utils/__init__.py +0 -0
  103. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/input_converters/workspace.py +0 -0
  104. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/input_savers/INPUT_RULES.md +0 -0
  105. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/input_savers/__init__.py +0 -0
  106. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/input_savers/components/__init__.py +0 -0
  107. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/input_savers/components/base.py +0 -0
  108. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/input_savers/components/methods.py +0 -0
  109. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/input_savers/components/verifier.py +0 -0
  110. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/input_savers/pipeline.py +0 -0
  111. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/input_savers/utils/__init__.py +0 -0
  112. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/input_savers/utils/guards.py +0 -0
  113. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/knowledge_graph/__init__.py +0 -0
  114. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/knowledge_graph/components/__init__.py +0 -0
  115. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/knowledge_graph/components/base.py +0 -0
  116. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/knowledge_graph/components/builder.py +0 -0
  117. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/knowledge_graph/components/retriever.py +0 -0
  118. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/knowledge_graph/components/workspace.py +0 -0
  119. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/knowledge_graph/pipeline.py +0 -0
  120. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/knowledge_graph/utils/__init__.py +0 -0
  121. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/latency_tracker/__init__.py +0 -0
  122. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/latency_tracker/pipeline.py +0 -0
  123. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_scheduler/__init__.py +0 -0
  124. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_scheduler/api.py +0 -0
  125. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_scheduler/code.py +0 -0
  126. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_scheduler/core.py +0 -0
  127. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_scheduler/local.py +0 -0
  128. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_scheduler/pipeline.py +0 -0
  129. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_scheduler/types.py +0 -0
  130. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_wrapper/__init__.py +0 -0
  131. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_wrapper/adapter.py +0 -0
  132. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_wrapper/components/__init__.py +0 -0
  133. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_wrapper/components/adapter_types.py +0 -0
  134. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_wrapper/components/base.py +0 -0
  135. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_wrapper/components/features.py +0 -0
  136. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_wrapper/components/litellm_wrapper.py +0 -0
  137. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_wrapper/components/openrouter_wrapper.py +0 -0
  138. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_wrapper/modes.py +0 -0
  139. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_wrapper/pipeline.py +0 -0
  140. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_wrapper/utils/__init__.py +0 -0
  141. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_wrapper/utils/registry.py +0 -0
  142. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/llm_wrapper/utils/token_counting.py +0 -0
  143. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/local_llm/__init__.py +0 -0
  144. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/local_llm/components/__init__.py +0 -0
  145. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/local_llm/components/huggingface_local.py +0 -0
  146. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/local_llm/components/local_llm.py +0 -0
  147. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/logging_setup.py +0 -0
  148. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/memory/__init__.py +0 -0
  149. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/memory/components/__init__.py +0 -0
  150. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/memory/components/lru_k.py +0 -0
  151. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/memory/components/manager.py +0 -0
  152. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/memory/components/note.py +0 -0
  153. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/memory/components/storage_manager.py +0 -0
  154. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/memory/pipeline.py +0 -0
  155. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/memory/utils/__init__.py +0 -0
  156. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/memory/utils/keyword_overlap.py +0 -0
  157. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/memory_tracker/__init__.py +0 -0
  158. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/memory_tracker/components/__init__.py +0 -0
  159. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/memory_tracker/components/process.py +0 -0
  160. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/memory_tracker/pipeline.py +0 -0
  161. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/metrics/__init__.py +0 -0
  162. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/metrics/components/__init__.py +0 -0
  163. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/metrics/components/collector.py +0 -0
  164. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/metrics/components/exporter.py +0 -0
  165. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/metrics/pipeline.py +0 -0
  166. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/metrics/utils/__init__.py +0 -0
  167. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/notes_manager/__init__.py +0 -0
  168. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/notes_manager/components/__init__.py +0 -0
  169. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/notes_manager/components/privacy.py +0 -0
  170. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/notes_manager/components/recall.py +0 -0
  171. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/notes_manager/components/tidy.py +0 -0
  172. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/notes_manager/pipeline.py +0 -0
  173. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/orchestrator/__init__.py +0 -0
  174. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/orchestrator/components/__init__.py +0 -0
  175. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/orchestrator/components/agent.py +0 -0
  176. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/orchestrator/components/cgroup_manager.py +0 -0
  177. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/orchestrator/components/context_interrupt.py +0 -0
  178. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/orchestrator/components/limits.py +0 -0
  179. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/orchestrator/components/scheduler.py +0 -0
  180. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/orchestrator/components/syscall_scheduler.py +0 -0
  181. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/orchestrator/pipeline.py +0 -0
  182. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/orchestrator/utils/__init__.py +0 -0
  183. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/orchestrator/utils/device.py +0 -0
  184. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/orchestrator/utils/protocol.py +0 -0
  185. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/os_kernel/__init__.py +0 -0
  186. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/os_kernel/filesystem.py +0 -0
  187. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/os_kernel/network.py +0 -0
  188. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/os_kernel/pipeline.py +0 -0
  189. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/os_kernel/sysinfo.py +0 -0
  190. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/paths.py +0 -0
  191. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/pipeline.py +0 -0
  192. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/py.typed +0 -0
  193. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/redact.py +0 -0
  194. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/sdk/__init__.py +0 -0
  195. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/sdk/adapters.py +0 -0
  196. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/sdk/agent.py +0 -0
  197. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/sdk/components/__init__.py +0 -0
  198. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/sdk/components/autogen_adapter.py +0 -0
  199. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/sdk/components/autogen_client.py +0 -0
  200. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/sdk/components/framework_adapter_base.py +0 -0
  201. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/sdk/components/metagpt_adapter.py +0 -0
  202. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/sdk/components/open_interpreter_adapter.py +0 -0
  203. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/sdk/components/tool_catalog.py +0 -0
  204. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/sdk/os_api.py +0 -0
  205. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/sdk/patterns.py +0 -0
  206. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/sdk/pipeline.py +0 -0
  207. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/sdk/queries.py +0 -0
  208. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/sdk/savers.py +0 -0
  209. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/sdk/utils/__init__.py +0 -0
  210. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/storage_manager/__init__.py +0 -0
  211. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/storage_manager/components/__init__.py +0 -0
  212. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/storage_manager/components/base.py +0 -0
  213. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/storage_manager/components/local_file_backend.py +0 -0
  214. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/storage_manager/components/storage_queue.py +0 -0
  215. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/storage_manager/components/vector_store_backend.py +0 -0
  216. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/storage_manager/pipeline.py +0 -0
  217. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/storage_manager/utils/__init__.py +0 -0
  218. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/storage_manager/utils/memory_adapter.py +0 -0
  219. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/storage_manager/utils/sdk.py +0 -0
  220. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/studio/__init__.py +0 -0
  221. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/studio/builder.py +0 -0
  222. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/studio/catalog.py +0 -0
  223. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/studio/checks.py +0 -0
  224. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/studio/clients.py +0 -0
  225. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/studio/editor.py +0 -0
  226. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/studio/files.py +0 -0
  227. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/studio/models.py +0 -0
  228. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/studio/plan.py +0 -0
  229. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/studio/planner.py +0 -0
  230. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/studio/policy.py +0 -0
  231. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/task_router/__init__.py +0 -0
  232. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/task_router/components/__init__.py +0 -0
  233. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/task_router/components/answerer.py +0 -0
  234. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/task_router/components/base.py +0 -0
  235. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/task_router/components/classifier.py +0 -0
  236. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/task_router/components/gates.py +0 -0
  237. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/task_router/pipeline.py +0 -0
  238. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/task_router/utils/__init__.py +0 -0
  239. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/__init__.py +0 -0
  240. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/backend.py +0 -0
  241. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/clients.py +0 -0
  242. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/commands.py +0 -0
  243. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/decide.py +0 -0
  244. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/files.py +0 -0
  245. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/flow_cli.py +0 -0
  246. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/hardware.py +0 -0
  247. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/history.py +0 -0
  248. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/keys.py +0 -0
  249. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/local_models.py +0 -0
  250. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/models.py +0 -0
  251. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/policy.py +0 -0
  252. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/project.py +0 -0
  253. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/review.py +0 -0
  254. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/security.py +0 -0
  255. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/syntax.py +0 -0
  256. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/theme.py +0 -0
  257. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/voice.py +0 -0
  258. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/terminal/workflow.py +0 -0
  259. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/token_tracker/__init__.py +0 -0
  260. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/token_tracker/pipeline.py +0 -0
  261. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/tokens.py +0 -0
  262. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/tool_catalog.py +0 -0
  263. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/tool_manager/__init__.py +0 -0
  264. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/tool_manager/builtin.py +0 -0
  265. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/tool_manager/components/__init__.py +0 -0
  266. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/tool_manager/components/base.py +0 -0
  267. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/tool_manager/components/conflict_resolver.py +0 -0
  268. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/tool_manager/components/param_validator.py +0 -0
  269. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/tool_manager/components/tool_call_queue.py +0 -0
  270. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/tool_manager/components/tool_registry.py +0 -0
  271. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/tool_manager/pipeline.py +0 -0
  272. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/tool_manager/utils/__init__.py +0 -0
  273. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/web/__init__.py +0 -0
  274. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/web/static/editor.js +0 -0
  275. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/web/static/fonts/LICENSE-Inter.txt +0 -0
  276. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/web/static/fonts/inter-400.woff2 +0 -0
  277. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/web/static/fonts/inter-500.woff2 +0 -0
  278. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/web/static/fonts/inter-600.woff2 +0 -0
  279. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/web/static/fonts/inter-700.woff2 +0 -0
  280. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/web/static/inter.css +0 -0
  281. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/web/static/pal-logo-256.png +0 -0
  282. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/web/static/pal-logo-32.png +0 -0
  283. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/web/static/pal-logo-64.png +0 -0
  284. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/web/static/studio.js +0 -0
  285. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal/web/static/voice.js +0 -0
  286. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal_code.egg-info/dependency_links.txt +0 -0
  287. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal_code.egg-info/entry_points.txt +0 -0
  288. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal_code.egg-info/requires.txt +0 -0
  289. {pal_code-0.1.1 → pal_code-0.1.2}/src/pal_code.egg-info/top_level.txt +0 -0
  290. {pal_code-0.1.1 → pal_code-0.1.2}/tests/test_cli.py +0 -0
  291. {pal_code-0.1.1 → pal_code-0.1.2}/tests/test_config.py +0 -0
  292. {pal_code-0.1.1 → pal_code-0.1.2}/tests/test_lockfile.py +0 -0
  293. {pal_code-0.1.1 → pal_code-0.1.2}/tests/test_paths.py +0 -0
  294. {pal_code-0.1.1 → pal_code-0.1.2}/tests/test_redact.py +0 -0
  295. {pal_code-0.1.1 → pal_code-0.1.2}/tests/test_run_script.py +0 -0
  296. {pal_code-0.1.1 → pal_code-0.1.2}/tests/test_tool_catalog.py +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Nandini Singh
3
+ Copyright (c) 2026 Vrushal More
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pal-code
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Pal: build and plan with AI in your terminal (pal cli) or browser (pal web), on a layer that cuts tokens and latency.
5
- Author: Nandini Singh
5
+ Author: Vrushal More
6
6
  License: MIT
7
7
  Keywords: ai,coding-assistant,cli,llm,ollama,tokens,code-graph
8
8
  Classifier: Development Status :: 4 - Beta
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pal-code"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "Pal: build and plan with AI in your terminal (pal cli) or browser (pal web), on a layer that cuts tokens and latency."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = { text = "MIT" }
12
- authors = [{ name = "Nandini Singh" }]
12
+ authors = [{ name = "Vrushal More" }]
13
13
  keywords = ["ai", "coding-assistant", "cli", "llm", "ollama", "tokens", "code-graph"]
14
14
  classifiers = [
15
15
  "Development Status :: 4 - Beta",
@@ -13,7 +13,7 @@ def _version() -> str:
13
13
  return version(name)
14
14
  except PackageNotFoundError:
15
15
  continue
16
- return "0.1.1" # a source checkout that is not installed
16
+ return "0.1.2" # a source checkout that is not installed
17
17
 
18
18
 
19
19
  __version__ = _version()
@@ -212,7 +212,7 @@ def load_dotenv(path: str | os.PathLike | None = None, env=None) -> list[str]:
212
212
  return loaded
213
213
 
214
214
 
215
- def _serve(settings: Settings, open_browser: bool = False, page: str = "", graph=None, documents: list | None = None) -> int:
215
+ def _serve(settings: Settings, open_browser: bool = False, page: str = "", graph=None, documents: list | None = None, project_dir=None) -> int:
216
216
  from pal.chat import ChatService
217
217
  from pal.web import create_app
218
218
 
@@ -221,7 +221,7 @@ def _serve(settings: Settings, open_browser: bool = False, page: str = "", graph
221
221
  service = ChatService(settings, graph=graph) if graph is not None else ChatService(settings)
222
222
  if documents:
223
223
  service.preload_docs = list(documents)
224
- app = create_app(service)
224
+ app = create_app(service, studio={"project_dir": project_dir}) if project_dir else create_app(service)
225
225
  if s.host not in _LOOPBACK:
226
226
  print(f"WARNING: listening on {s.host}. Pal has no login: anyone who can reach this port can use "
227
227
  "your API keys' quota and read stored memory. Put it behind an authenticating reverse proxy.",
@@ -239,6 +239,9 @@ def _serve(settings: Settings, open_browser: bool = False, page: str = "", graph
239
239
  "the app still works and sends everything to the API model.", file=sys.stderr)
240
240
  else:
241
241
  print(f"Local model: {status['detail']}")
242
+ import threading as _t
243
+ from pal.context_window import context_window
244
+ _t.Thread(target=lambda: context_window("openai", "gpt-4o"), daemon=True).start() # loads LiteLLM's model list now, not on the first page load
242
245
  if open_browser:
243
246
  import threading
244
247
  import webbrowser
@@ -0,0 +1,42 @@
1
+ """How many tokens a model can take in one request (its context window), for the "tokens left" counters in the terminal and the web page.
2
+
3
+ API models: the size LiteLLM knows for the model, else a safe default. Local models: the context Pal gives Ollama (num_ctx), which is what
4
+ really limits them here, whatever the model could do on paper."""
5
+ from __future__ import annotations
6
+
7
+ from functools import lru_cache
8
+
9
+ LOCAL_CONTEXT = 8192 # Pal runs local models with num_ctx 8192 (pal.terminal.backend.make_core)
10
+ API_DEFAULT = 128_000 # a model LiteLLM does not know: most current API models take at least this
11
+ LOCAL_PROVIDERS = ("local", "ollama", "lmstudio", "vllm", "llamacpp", "llama.cpp", "huggingface")
12
+
13
+
14
+ @lru_cache(maxsize=256)
15
+ def context_window(provider: str, model: str) -> int:
16
+ provider = (provider or "").lower()
17
+ if provider in LOCAL_PROVIDERS:
18
+ return LOCAL_CONTEXT
19
+ if not model:
20
+ return API_DEFAULT
21
+ try:
22
+ import litellm
23
+ for name in (f"{provider}/{model}" if provider else model, model, model.split("/")[-1]):
24
+ try:
25
+ info = litellm.get_model_info(name)
26
+ except Exception: # noqa: BLE001 (not in LiteLLM's table under this name)
27
+ continue
28
+ size = info.get("max_input_tokens") or info.get("max_tokens")
29
+ if size:
30
+ return int(size)
31
+ except Exception: # noqa: BLE001 (LiteLLM missing or broken: the default)
32
+ pass
33
+ return API_DEFAULT
34
+
35
+
36
+ def tokens_left(window: int, last_prompt: int) -> int:
37
+ """What is left of the window after the last request's input (the next one is usually at least as big)."""
38
+ return max(0, window - max(0, last_prompt))
39
+
40
+
41
+ def short(n: int) -> str:
42
+ return f"{n / 1000:.1f}k".replace(".0k", "k") if n >= 10_000 else f"{n:,}"
@@ -49,7 +49,7 @@ def clean_references(raw) -> list[dict]:
49
49
 
50
50
 
51
51
  def register_studio(app, service, *, planner_core: Callable | None = None, core_factory: Callable | None = None, discover_fn: Callable | None = None,
52
- hot_turns: int | None = None) -> RunStore:
52
+ hot_turns: int | None = None, project_dir: Path | None = None) -> RunStore:
53
53
  """Add the Studio page and endpoints to the app. The three callables are for tests: the planner's model, the builder's model, local-model discovery."""
54
54
  cfg = service.settings
55
55
  home = Path(cfg.storage_dir).expanduser() if cfg.storage_dir else data_dir()
@@ -71,7 +71,7 @@ def register_studio(app, service, *, planner_core: Callable | None = None, core_
71
71
  raise UserInputError("no local model is chosen: start Ollama and pull a model, or switch to an API model")
72
72
  return local_core(m["model"], num_ctx())
73
73
 
74
- runs = RunStore(home / "studio", core_factory or default_core_factory, max_runs=cfg.studio.max_runs, hot_turns=hot_turns)
74
+ runs = RunStore(home / "studio", core_factory or default_core_factory, max_runs=cfg.studio.max_runs, hot_turns=hot_turns, project_dir=project_dir)
75
75
  app.extensions["studio_runs"] = runs
76
76
 
77
77
  def local() -> dict:
@@ -95,9 +95,22 @@ class Run:
95
95
  return self.arms[name]
96
96
 
97
97
 
98
+ def build_target(project: Path, folder: str) -> Path:
99
+ """Where a build is written in the folder Pal was started in: the folder itself when it is empty, else a new subfolder named after the
100
+ product (with -2, -3 ... when that name is taken), so nothing that is already there is overwritten."""
101
+ project = Path(project)
102
+ if not any(p for p in project.iterdir() if not p.name.startswith(".")):
103
+ return project
104
+ target, n = project / folder, 2
105
+ while target.exists() and (not target.is_dir() or any(target.iterdir())):
106
+ target, n = project / f"{folder}-{n}", n + 1
107
+ return target
108
+
109
+
98
110
  class RunStore:
99
- def __init__(self, root: Path, core_factory: Callable, max_runs: int = 6, hot_turns: int | None = None):
111
+ def __init__(self, root: Path, core_factory: Callable, max_runs: int = 6, hot_turns: int | None = None, project_dir: Path | None = None):
100
112
  self.root, self.core_factory, self.max_runs, self.hot_turns = Path(root), core_factory, max_runs, hot_turns
113
+ self.project_dir = Path(project_dir).resolve() if project_dir else None # pal web's folder: the build is written there
101
114
  self._runs: dict[str, Run] = {}
102
115
  self._editors: dict[tuple[str, str], EditorSession] = {}
103
116
  self._lock = threading.Lock()
@@ -140,11 +153,17 @@ class RunStore:
140
153
  break
141
154
  policy = Policy(dict(plan["permissions"]))
142
155
  save = plan["permissions"].get("write_project")
143
- state = ArmState(arm, label, (self.root / run.id / arm / plan["product"]["folder"]) if save else None)
156
+ where = None
157
+ if save:
158
+ where = (build_target(self.project_dir, plan["product"]["folder"]) if self.project_dir and arm != "baseline"
159
+ else self.root / run.id / arm / plan["product"]["folder"]) # the plain client's comparison copy stays in Pal's folder
160
+ state = ArmState(arm, label, where)
144
161
  run.arms[arm] = state
145
162
  if state.workspace:
146
163
  state.workspace.mkdir(parents=True, exist_ok=True)
147
164
  run.emit("arm_start", f"{label}: starting" if twice else "Starting", arm=arm)
165
+ if state.workspace and self.project_dir and arm != "baseline":
166
+ run.emit("saved_to", f"Files are written to {state.workspace}", arm=arm, path=str(state.workspace))
148
167
  client: PlainClient | PalClient
149
168
  if arm == "baseline":
150
169
  client = PlainClient(core, SYSTEM)
@@ -67,6 +67,7 @@ class BenchResult:
67
67
  plain: Arm
68
68
  pal: Arm
69
69
  when: float = field(default_factory=time.time)
70
+ pal_files: dict = field(default_factory=dict, repr=False) # what Pal's way wrote: {path: bytes}, offered to keep in your folder
70
71
 
71
72
  @property
72
73
  def saved(self) -> int:
@@ -78,6 +79,7 @@ class BenchResult:
78
79
 
79
80
  def to_dict(self) -> dict:
80
81
  d = asdict(self)
82
+ d.pop("pal_files", None)
81
83
  d.update(saved=self.saved, saved_pct=self.saved_pct, plain_total=self.plain.total, pal_total=self.pal.total)
82
84
  return d
83
85
 
@@ -161,7 +163,12 @@ def run_bench(folder: Path, task: str, core_factory: Callable[[], Any], model: s
161
163
  pal_arm = _run_arm("pal", pal, steps, progress)
162
164
  finally:
163
165
  client.close()
164
- result = BenchResult(task, model, str(folder), plain_arm, pal_arm)
166
+ kept = {}
167
+ for rel in sorted({f for st in pal_arm.steps for f in st.files}):
168
+ f = pal_dir / rel
169
+ if f.is_file():
170
+ kept[rel] = f.read_bytes()
171
+ result = BenchResult(task, model, str(folder), plain_arm, pal_arm, pal_files=kept)
165
172
  save(result, home)
166
173
  return result
167
174
 
@@ -70,7 +70,7 @@ def build_parser() -> argparse.ArgumentParser:
70
70
  a.add_argument("--build", action="store_true", help="use the Build agent: it may propose file changes (asks before writing)")
71
71
  b = sub.add_parser("bench", help="run one task as a plain chat and through Pal, on the same model, and compare the real tokens")
72
72
  _common(b, suppress=True)
73
- b.add_argument("task", nargs="+", help="what to build, as you would type it")
73
+ b.add_argument("task", nargs="*", help="what to build, as you would type it (leave it out to open the bench screen and type tasks there)")
74
74
  b.add_argument("--then", action="append", default=None, metavar="MESSAGE", help="a follow-up message (repeat for more; default: two standard ones)")
75
75
  b.add_argument("--no-follow-ups", action="store_true", help="only the task itself")
76
76
  b.add_argument("--json", action="store_true", help="print the result as JSON")
@@ -79,13 +79,17 @@ def build_parser() -> argparse.ArgumentParser:
79
79
  return p
80
80
 
81
81
 
82
- def bench(args) -> int:
83
- """pal bench: the same task, plain chat versus Pal, on the same model."""
82
+ def bench(args, core_factory: Callable | None = None) -> int:
83
+ """pal bench: the same task, plain chat versus Pal, on the same model. Without a task it opens the usual Pal screen (\"pal bench\"), and every
84
+ task typed there is run both ways, with the results as a table and bar charts."""
84
85
  import json as _json
85
86
 
86
- from rich.table import Table
87
-
88
- from pal.terminal.bench import run_bench
87
+ if not args.task:
88
+ if not (sys.stdin.isatty() and sys.stdout.isatty()):
89
+ print("pal bench: give a task, e.g. pal bench \"make a todo app\"", file=sys.stderr)
90
+ return 2
91
+ args.bench_screen = True
92
+ return interactive(args, core_factory)
89
93
  console = ui.make_console()
90
94
  host = args.host or backend.DEFAULT_HOST
91
95
  prefs = backend.load_prefs()
@@ -103,17 +107,36 @@ def bench(args) -> int:
103
107
  except PalError as e:
104
108
  print(f"pal bench: {e}", file=sys.stderr)
105
109
  return 2
106
- follow = [] if args.no_follow_ups else args.then
107
- task = " ".join(args.task)
108
- status = console.status(Text("Starting\u2026", style=theme.MUTED), spinner="dots", spinner_style=theme.RED)
109
- with status:
110
- result = run_bench(Path(args.dir), task, lambda: backend.make_core(provider, model, host), model, follow,
111
- progress=lambda m: status.update(Text(m, style=theme.MUTED)))
110
+ result = bench_run(console, Path(args.dir), " ".join(args.task), provider, model, host, [] if args.no_follow_ups else args.then)
112
111
  if args.json:
113
112
  print(_json.dumps(result.to_dict(), indent=1))
114
113
  return 0
115
- ui.say(console, Text.assemble(("pal bench ", f"bold {theme.WHITE}"), (f"{model} ({provider}) \u00B7 {ui.short_path(Path(args.dir).resolve())}", theme.MUTED)))
116
- ui.say(console, Text(f"Task: {task}", style=theme.INK))
114
+ show_bench(console, result, provider, model, Path(args.dir))
115
+ return 0
116
+
117
+
118
+ def bench_run(console, folder: Path, task: str, provider: str, model: str, host: str, follow=None, core_factory: Callable | None = None):
119
+ from pal.terminal.bench import run_bench
120
+ status = console.status(Text("Starting\u2026", style=theme.MUTED), spinner="dots", spinner_style=theme.RED)
121
+ with status:
122
+ return run_bench(folder, task, lambda: (core_factory or backend.make_core)(provider, model, host), model, follow,
123
+ progress=lambda m: status.update(Text(m, style=theme.MUTED)))
124
+
125
+
126
+ def bars(console, title: str, rows: list[tuple[str, float, str]], width: int = 46) -> None:
127
+ """A horizontal bar chart in the terminal: (label, value, shown value). Plain chat in grey, Pal in red."""
128
+ top = max((v for _, v, _ in rows), default=0) or 1
129
+ ui.say(console, Text(title, style=f"bold {theme.WHITE}"))
130
+ for label, value, shown in rows:
131
+ n = max(1, round(width * value / top)) if value > 0 else 0
132
+ colour = theme.RED if label.lower().startswith("pal") else theme.MUTED
133
+ ui.say(console, Text.assemble((" " + f"{label:<11}", theme.INK), ("\u2588" * n, colour), (" " * (width - n + 2), ""), (shown, f"bold {theme.WHITE}")))
134
+
135
+
136
+ def show_bench(console, result, provider: str, model: str, folder: Path) -> None:
137
+ from rich.table import Table
138
+ ui.say(console, Text.assemble(("pal bench ", f"bold {theme.WHITE}"), (f"{model} ({provider}) \u00B7 {ui.short_path(Path(folder).resolve())}", theme.MUTED)))
139
+ ui.say(console, Text(f"Task: {result.task}", style=theme.INK))
117
140
  console.print()
118
141
  grid = Table(box=None, padding=(0, 2), header_style=f"bold {theme.WHITE}")
119
142
  for col, just in (("step", "left"), ("agent", "left"), ("plain chat", "right"), ("Pal", "right"), ("saved", "right")):
@@ -129,6 +152,13 @@ def bench(args) -> int:
129
152
  Text(f"{result.saved:,} ({result.saved_pct:g}%)", style=f"bold {theme.RED_HI}"))
130
153
  ui.say(console, grid)
131
154
  console.print()
155
+ bars(console, "Tokens", [("plain chat", result.plain.total, f"{result.plain.total:,}"), ("Pal", result.pal.total, f"{result.pal.total:,}")])
156
+ console.print()
157
+ bars(console, "Time", [("plain chat", result.plain.seconds, f"{result.plain.seconds:g}s"), ("Pal", result.pal.seconds, f"{result.pal.seconds:g}s")])
158
+ console.print()
159
+ verdict = (f"Pal used {result.saved:,} fewer tokens ({result.saved_pct:g}% less)." if result.saved > 0 else
160
+ f"Pal used {-result.saved:,} more tokens on this task (it pays off with bigger projects, documents and longer conversations).")
161
+ ui.say(console, Text(verdict, style=f"bold {theme.RED_HI}"))
132
162
  for arm in (result.plain, result.pal):
133
163
  files = sorted({f for s in arm.steps for f in s.files})
134
164
  ui.say(console, Text(f"{arm.name:<6} {arm.seconds:g}s \u00B7 {arm.calls} model call{'s' if arm.calls != 1 else ''} \u00B7 wrote: {', '.join(files) or 'no files'}"
@@ -137,7 +167,28 @@ def bench(args) -> int:
137
167
  ui.say(console, Text("Token counts are the model's own." if not est else f"{est} call(s) had no token count from the model, so those are estimates (characters / 4).", style=theme.MUTED))
138
168
  ui.say(console, Text("Pal's local helper jobs are off in the bench, so its saving here comes from the project map, graph, documents and history alone.", style=theme.MUTED))
139
169
  console.print()
140
- return 0
170
+
171
+
172
+ def keep_bench_files(console, result, folder: Path) -> list[str]:
173
+ """Both ways build in temporary copies; offer to write Pal's version into the folder (asking first, never overwriting without saying so)."""
174
+ if not result.pal_files:
175
+ return []
176
+ folder = Path(folder).resolve()
177
+ clash = [p for p in result.pal_files if (folder / p).exists()]
178
+ question = f"Save Pal's version ({len(result.pal_files)} file{'s' if len(result.pal_files) != 1 else ''}) in {ui.short_path(folder)}?" + (
179
+ f" It replaces {len(clash)} existing file{'s' if len(clash) != 1 else ''}." if clash else "")
180
+ if not ui.confirm(console, question):
181
+ return []
182
+ written = []
183
+ for rel, data in result.pal_files.items():
184
+ target = (folder / rel).resolve()
185
+ if folder not in target.parents:
186
+ continue # never outside the folder
187
+ target.parent.mkdir(parents=True, exist_ok=True)
188
+ target.write_bytes(data)
189
+ written.append(rel)
190
+ ui.say(console, Text(f"Saved: {', '.join(written)}", style=theme.INK))
191
+ return written
141
192
 
142
193
 
143
194
  def setup(args) -> int:
@@ -165,7 +216,7 @@ def first_run() -> None:
165
216
  console.print()
166
217
 
167
218
 
168
- def prepare_folder(args, console, core_factory: Callable | None = None) -> tuple[str, Project | None]:
219
+ def prepare_folder(args, console, core_factory: Callable | None = None, choose: bool = True) -> tuple[str, Project | None]:
169
220
  """Before a session: get local models ready (install and start Ollama, download the models for this GPU, asking first), then set up the
170
221
  folder: documents to markdown, the project map, the code graph (in the background). Returns (a line about the local model, the Project)."""
171
222
  from pal.terminal.project import Project
@@ -173,7 +224,7 @@ def prepare_folder(args, console, core_factory: Callable | None = None) -> tuple
173
224
  local = ""
174
225
  if core_factory is None:
175
226
  tty = sys.stdin.isatty() and sys.stdout.isatty()
176
- local = ensure_local(console, backend.hardware(), args.host or backend.DEFAULT_HOST, interactive=tty).detail
227
+ local = ensure_local(console, backend.hardware(), args.host or backend.DEFAULT_HOST, interactive=tty, choose=choose).detail
177
228
  folder = Path(getattr(args, "dir", ".") or ".")
178
229
  if os.environ.get("PAL_SKIP_PROJECT"):
179
230
  return local, None
@@ -217,7 +268,7 @@ def web(args) -> int:
217
268
  from pal.terminal.project import ProjectGraph
218
269
  graph = ProjectGraph(project.root / "graph", settings.knowledge_graph)
219
270
  return _serve(settings, open_browser=not args.no_open, page="studio" if args.page == "studio" else "", graph=graph,
220
- documents=project.documents() if project is not None else None)
271
+ documents=project.documents() if project is not None else None, project_dir=Path(getattr(args, "dir", ".") or ".").resolve())
221
272
 
222
273
 
223
274
  def _utf8() -> None:
@@ -488,6 +539,14 @@ def handle(line: str, args, session: PalSession, console, core_factory, box: dic
488
539
  return session, True
489
540
  if cmd == "/help":
490
541
  ui.show_help(console)
542
+ elif cmd == "/agent":
543
+ from pal.terminal.setup import choose_agent
544
+ mode = rest if rest in MODES else choose_agent(console, current=session.mode)
545
+ if mode:
546
+ session.mode = mode
547
+ backend.save_prefs(mode=mode)
548
+ ui.say(console, Text(f"Agent: {mode.capitalize()} ({AGENT_ABOUT[mode]})", style=theme.MUTED))
549
+ console.print()
491
550
  elif cmd == "/mode":
492
551
  if rest in MODES:
493
552
  session.mode = rest
@@ -707,13 +766,37 @@ def voice_flow(console) -> str:
707
766
  return heard.text
708
767
 
709
768
 
769
+ def choosing(args, core_factory: Callable | None = None) -> bool:
770
+ """pal cli asks for its models at every start (nothing preselected), unless they were given on the command line or this is not a terminal."""
771
+ return (core_factory is None and not args.provider and not args.model and sys.stdin.isatty() and sys.stdout.isatty()
772
+ and not os.environ.get("PAL_SKIP_SETUP") and not api_only(backend.hardware()))
773
+
774
+
775
+ def ensure_ready_line(args) -> str:
776
+ """The local-model line again, after the models were chosen (and maybe downloaded)."""
777
+ from pal.terminal.setup import ensure_local
778
+ return ensure_local(ui.make_console(), backend.hardware(), args.host or backend.DEFAULT_HOST, interactive=False, choose=False).detail
779
+
780
+
710
781
  def interactive(args, core_factory: Callable | None = None) -> int:
711
782
  console = ui.make_console()
712
- local, project = prepare_folder(args, console, core_factory)
783
+ ask = choosing(args, core_factory)
784
+ local, project = prepare_folder(args, console, core_factory, choose=not ask)
785
+ if ask:
786
+ from pal.terminal.setup import choose_models
787
+ console.print()
788
+ if not choose_models(console, backend.hardware(), args.host or backend.DEFAULT_HOST):
789
+ console.print(Text("\nNo model chosen. Run pal cli again to choose one.\n", style=theme.MUTED))
790
+ return 1
791
+ if not args.mode:
792
+ from pal.terminal.setup import choose_agent
793
+ args.mode = choose_agent(console) or None
794
+ local = ensure_ready_line(args)
713
795
  session = start(args, core_factory, project)
714
796
  if session is None:
715
797
  return 1
716
798
  session.local_status = local
799
+ session.brand = "bench" if getattr(args, "bench_screen", False) else "cli"
717
800
  if project is not None:
718
801
  ui.show_project(console, session)
719
802
  console.print()
@@ -753,6 +836,14 @@ def interactive(args, core_factory: Callable | None = None) -> int:
753
836
  session, go = handle(text, args, session, console, core_factory, box)
754
837
  if not go:
755
838
  break
839
+ elif text and getattr(args, "bench_screen", False): # pal bench: the task both ways, then the comparison
840
+ host = args.host or backend.DEFAULT_HOST
841
+ try:
842
+ result = bench_run(console, session.cwd, text, session.provider, session.model, host, core_factory=core_factory)
843
+ show_bench(console, result, session.provider, session.model, session.cwd)
844
+ keep_bench_files(console, result, session.cwd)
845
+ except PalError as e:
846
+ ui.say(console, Text(f"The bench stopped: {e}", style=theme.RED_HI))
756
847
  elif text:
757
848
  flow_cli.run_turn(box["wf"], text, console)
758
849
  except KeyboardInterrupt: # Esc or Ctrl+C at an awkward moment: back to the box, never out of Pal
@@ -813,7 +904,7 @@ def main(argv: Sequence[str] | None = None, core_factory: Callable | None = None
813
904
  if args.cmd == "setup":
814
905
  return setup(args)
815
906
  if args.cmd == "bench":
816
- return bench(args)
907
+ return bench(args, core_factory)
817
908
  if args.cmd == "ask":
818
909
  return ask_once(args, core_factory)
819
910
  if not (sys.stdin.isatty() and sys.stdout.isatty()) and core_factory is None:
@@ -0,0 +1,140 @@
1
+ """The Pal logo (two people hugging, white and red with a black outline) as pixels for the terminal.
2
+
3
+ Drawn from the logo image at 24 x 26 pixels with half blocks, two pixel rows per text line, so it shows in any terminal with colour and needs no
4
+ image support. W white, R red, K the black outline, . see-through (the terminal's own background).
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from pal.terminal import theme
9
+
10
+ GRID = [
11
+ '......KKKK.....K........',
12
+ '.....KWWWWWK.KRRRK......',
13
+ '....KWWWWWWWKRRRRRK.....',
14
+ '....KWWWWWWWKRRRRRRK....',
15
+ '....KWWWWWWWWKRRRRRR....',
16
+ '....KWWWWWWWWKRRRRRR....',
17
+ '....KWWWWWWWWKRRRRRR....',
18
+ '....KWWWWWWWWWRRRRRR....',
19
+ '....KWWWWWWWWKRRRRRK....',
20
+ '...KRKWWWWWWWKRRRRKKK...',
21
+ '..KRRKKWWWWWWKRRRKWWK...',
22
+ '..KRRRKKWWWWKRRRRKWWK...',
23
+ '...KRRRRKKKKRRRRRKKKK...',
24
+ '..KKKRRRRRRRRRRRRRRRRK..',
25
+ '..WWWKRRRRRRRRKKWKRRRR..',
26
+ '.KWWWWKRRRRRRKWWWKRRRRK.',
27
+ '.KWWWWWKKRRRKWWWWKRRRRR.',
28
+ 'KWWWWWWWWKKKWWWWKRRRRRRK',
29
+ 'KWWWWWWWWWWWWWWWKRRRRRRK',
30
+ 'KWWWWWWWWWWWWWWKRRRRRRRK',
31
+ 'KWWWWWWWWWWWWWKRRRRRRRRK',
32
+ 'KWWWWWWWWWWWWKRRRRRRRRRK',
33
+ 'KWWWWWWWWWKKRRRRRRRRRRRK',
34
+ 'KWWWWWWWWKRRRRRRRRRRRRRK',
35
+ 'KWWWWWWWWKRRRRRRRRRRRRRK',
36
+ 'KKKKKKKKKKKKKKKKKKKKKKKK',
37
+ ]
38
+ COLOURS = {"W": theme.WHITE, "R": theme.RED, "K": "#000000"}
39
+
40
+
41
+ def logo_lines(grid: list[str] | None = None) -> list[list[tuple[str, str]]]:
42
+ """Lines of (characters, style). The style is 'fg bg' in hex, or '' for nothing drawn: rich and prompt_toolkit both read it after a small change."""
43
+ g = grid or GRID
44
+ out: list[list[tuple[str, str]]] = []
45
+ for y in range(0, len(g), 2):
46
+ top, bottom = g[y], g[y + 1] if y + 1 < len(g) else "." * len(g[y])
47
+ line: list[tuple[str, str]] = []
48
+ for t, b in zip(top, bottom):
49
+ ct, cb = COLOURS.get(t), COLOURS.get(b)
50
+ if ct is None and cb is None:
51
+ line.append((" ", ""))
52
+ elif ct is None:
53
+ line.append(("▄", f"{cb}|"))
54
+ elif cb is None or ct == cb:
55
+ line.append(("▀" if cb is None else "█", f"{ct}|"))
56
+ else:
57
+ line.append(("▀", f"{ct}|{cb}"))
58
+ out.append(line)
59
+ return out
60
+
61
+
62
+ def rich_style(style: str) -> str:
63
+ fg, _, bg = style.partition("|")
64
+ return (fg + (f" on {bg}" if bg else "")) if fg else ""
65
+
66
+
67
+ def ptk_style(style: str) -> str:
68
+ fg, _, bg = style.partition("|")
69
+ return (f"fg:{fg}" + (f" bg:{bg}" if bg else "")) if fg else ""
70
+
71
+
72
+ # The start screen's logo: drawn from the logo image as solid pixels (half blocks, two pixel rows per text line), at two sizes.
73
+ LOGO = ['.....KKKK...........', '....KWWWWK.KRRR.....', '....WWWWWWKRRRRR....', '...KWWWWWWWKRRRRK...', '...KWWWWWWWKRRRRK...',
74
+ '...KWWWWWWWKRRRRK...', '....WWWWWWWKRRRRK...', '...KKWWWWWWKRRRRK...', '..KRKWWWWWWKRRRKW...', '..KRRKWWWWKRRRKWWK..',
75
+ '..KRRRKKKKRRRRKKK...', '..KKRRRRRRRRRKRRRK..', '.KWWKRRRRRRRKWKRRRK.', '.WWWWWKRRRRKWWKRRRR.', '.WWWWWWKRKKWWWKRRRRK',
76
+ 'KWWWWWWWWWWWWKRRRRRK', 'KWWWWWWWWWWWKRRRRRRK', 'KWWWWWWWWWWKRRRRRRRK', 'KWWWWWWWWWKRRRRRRRRK', 'KWWWWWWWKRRRRRRRRRRK',
77
+ 'KWWWWWWWKRRRRRRRRRRK']
78
+ LOGO_SMALL = ['....KK..K.....', '...WWWWKRRR...', '...WWWWKRRRK..', '..KWWWWWRRRR..', '..KWWWWWRRRR..', '..KWWWWWRRRK..', '..RKWWWKRRKW..',
79
+ '..RRKWKRRRKK..', '.KKRRRRRRKRRK.', '.WWWRRRRWWRRR.', '.WWWWKRWWWRRRR', 'WWWWWWWWWKRRRR', 'WWWWWWWWKRRRRR', 'WWWWWWWKRRRRRR',
80
+ 'WWWWWWRRRRRRRR', 'KWWWWKRRRRRRRK']
81
+
82
+ # The wordmark "pal code" in pixel letters. Rows 0-1 ascenders, 2-6 the letters, 7-8 the tail of the p.
83
+ FONT = {
84
+ "p": [".....", ".....", "####.", "#...#", "#...#", "#...#", "####.", "#....", "#....", "....."],
85
+ "a": [".....", ".....", ".###.", "....#", ".####", "#...#", ".####", ".....", ".....", "....."],
86
+ "l": ["##.", ".#.", ".#.", ".#.", ".#.", ".#.", ".##", "...", "...", "..."],
87
+ "c": [".....", ".....", ".####", "#....", "#....", "#....", ".####", ".....", ".....", "....."],
88
+ "o": [".....", ".....", ".###.", "#...#", "#...#", "#...#", ".###.", ".....", ".....", "....."],
89
+ "d": ["....#", "....#", ".####", "#...#", "#...#", "#...#", ".####", ".....", ".....", "....."],
90
+ "e": [".....", ".....", ".###.", "#...#", "#####", "#....", ".####", ".....", ".....", "....."],
91
+ "i": ["#", ".", "#", "#", "#", "#", "#", ".", ".", "."],
92
+ "w": [".......", ".......", "#.....#", "#.....#", "#..#..#", "#.#.#.#", ".#...#.", ".......", ".......", "......."],
93
+ "b": ["#....", "#....", "####.", "#...#", "#...#", "#...#", "####.", ".....", ".....", "....."],
94
+ "n": [".....", ".....", "####.", "#...#", "#...#", "#...#", "#...#", ".....", ".....", "....."],
95
+ "h": ["#....", "#....", "####.", "#...#", "#...#", "#...#", "#...#", ".....", ".....", "....."],
96
+ }
97
+ WORDS = {"cli": "pal code", "web": "pal code", "bench": "pal code"} # the wordmark everywhere: "pal" in white, "code" in red
98
+
99
+
100
+ def word_for(kind: str = "cli") -> list[str]:
101
+ text = WORDS.get(kind, WORDS["cli"])
102
+ return word_grid(text, "WWW " + "R" * (len(text) - 4))
103
+
104
+
105
+ def word_grid(text: str = "pal code", colours: str = "WWW RRRR") -> list[str]:
106
+ rows = [""] * 10
107
+ for ch, col in zip(text, colours):
108
+ if ch == " ":
109
+ rows = [r + "..." for r in rows]
110
+ continue
111
+ rows = [r + FONT[ch][y].replace("#", col) + "." for y, r in enumerate(rows)]
112
+ width = max(len(r.rstrip(".")) for r in rows)
113
+ return [r[:width] for r in rows]
114
+
115
+
116
+ def _see_through(grid: list[str]) -> list[str]:
117
+ """The black outline is left to the terminal's own background, so it matches any dark theme."""
118
+ return [r.replace("K", ".") for r in grid]
119
+
120
+
121
+ def banner(size: str = "full", kind: str = "cli") -> list[list[tuple[str, str]]]:
122
+ """The start screen's mark as lines of (characters, 'fg|bg') runs: the logo with the wordmark beside it ('full' or 'small'), or the
123
+ wordmark alone ('word')."""
124
+ word = logo_lines(word_for(kind))
125
+ if size == "word":
126
+ return word
127
+ art = logo_lines(_see_through(LOGO if size == "full" else LOGO_SMALL))
128
+ gap, top = 4, (len(art) - len(word)) // 2
129
+ blank_art = [(" " * len(art[0]), "")]
130
+ out = []
131
+ for i in range(max(len(art), len(word) + top)):
132
+ left = art[i] if i < len(art) else blank_art
133
+ right = word[i - top] if 0 <= i - top < len(word) else []
134
+ line = list(left) + [(" " * gap, "")] + list(right)
135
+ width = sum(len(c) for c, _ in line)
136
+ out.append(line + [(" " * (len(art[0]) + gap + len(word[0]) - width), "")]) # every line the same width, so centring keeps the shape
137
+ return out
138
+
139
+
140
+ BANNER_ROWS = {size: len(banner(size)) for size in ("full", "small", "word")}
@@ -169,6 +169,7 @@ class PalSession:
169
169
  self.history_tokens = 0 # what a plain chat would carry along: every earlier message and reply
170
170
  self.history_paste = 0 # the part of that history that is @files pasted into earlier messages
171
171
  self.turns = 0
172
+ self.last_prompt = 0 # the input tokens of the last request: how full the model's context was
172
173
  self.pal_tokens = self.helper_tokens = self.plain_tokens = 0
173
174
  self.cost_usd = 0.0 # what the model calls cost so far, when the provider's price is known
174
175
  self.seconds = 0.0
@@ -194,6 +195,17 @@ class PalSession:
194
195
  own = self.role_models.get(mode or self.mode)
195
196
  return own if own else (self.provider, self.model)
196
197
 
198
+ def tokens_left(self) -> tuple[int, int]:
199
+ """(tokens left in the active agent's context window, the window). Shown under the input box and in the status line."""
200
+ from pal.context_window import context_window, tokens_left
201
+ provider, model = self.model_for()
202
+ window = context_window(provider, model)
203
+ return tokens_left(window, self.last_prompt), window
204
+
205
+ def budget_left(self) -> int | None:
206
+ """What is left of the session's token cap (/budget), or None without one."""
207
+ return max(0, self.budget - self.pal_tokens) if self.budget else None
208
+
197
209
  def client_add_documents(self) -> None:
198
210
  """After /project rebuild: index the folder's documents and map again in this conversation."""
199
211
  add = getattr(self.client, "add_documents", None)
@@ -345,6 +357,7 @@ class PalSession:
345
357
  self.records.append(Record(text, reply.prompt_tokens + reply.completion_tokens, reply.helper_tokens, plain_prompt + reply.completion_tokens, saved_by,
346
358
  reply.seconds, getattr(reply, "route", "api")))
347
359
  self.last_reply = reply.text.strip()
360
+ self.last_prompt = reply.prompt_tokens or self.last_prompt
348
361
  turn = Turn(text=reply.text.strip(), warnings=warnings, prompt_tokens=reply.prompt_tokens, completion_tokens=reply.completion_tokens,
349
362
  helper_tokens=reply.helper_tokens, plain_estimate=plain_prompt + reply.completion_tokens, seconds=reply.seconds or round(time.perf_counter() - t0, 1), saved_by=saved_by)
350
363
  if self.mode == "ask": # the Ask agent may ask the person to choose, instead of guessing