agent-folder-workspace 0.1.0__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 (93) hide show
  1. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/PKG-INFO +45 -10
  2. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/README.md +44 -9
  3. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/docs/architecture.md +19 -7
  4. agent_folder_workspace-0.1.2/docs/superpowers/plans/2026-07-22-console-logging.md +270 -0
  5. agent_folder_workspace-0.1.2/docs/superpowers/plans/2026-07-23-runtime-workspace-selection.md +384 -0
  6. agent_folder_workspace-0.1.2/docs/superpowers/specs/2026-07-22-console-logging-design.md +91 -0
  7. agent_folder_workspace-0.1.2/docs/superpowers/specs/2026-07-23-runtime-workspace-selection-design.md +141 -0
  8. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/docs/windows.md +39 -1
  9. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/pyproject.toml +2 -2
  10. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/scripts/check_wheel.py +17 -2
  11. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/__init__.py +7 -1
  12. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/__main__.py +2 -2
  13. agent_folder_workspace-0.1.2/src/agent_folder_workspace/cli.py +229 -0
  14. agent_folder_workspace-0.1.2/src/agent_folder_workspace/console_logging.py +209 -0
  15. agent_folder_workspace-0.1.2/src/agent_folder_workspace/mcp_server.py +304 -0
  16. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/models.py +20 -0
  17. agent_folder_workspace-0.1.2/src/agent_folder_workspace/opencode/skill/SKILL.md +30 -0
  18. agent_folder_workspace-0.1.2/src/agent_folder_workspace/schemas/WorkspaceCloseV1.schema.json +27 -0
  19. agent_folder_workspace-0.1.2/src/agent_folder_workspace/schemas/WorkspaceHandleV1.schema.json +509 -0
  20. agent_folder_workspace-0.1.2/src/agent_folder_workspace/schemas/WorkspaceListV1.schema.json +538 -0
  21. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/workspace.py +13 -4
  22. agent_folder_workspace-0.1.2/src/agent_folder_workspace/workspace_manager.py +234 -0
  23. agent_folder_workspace-0.1.2/tests/test_console_logging.py +266 -0
  24. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/test_contracts.py +50 -0
  25. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/test_distribution.py +13 -3
  26. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/test_mcp_cli_installer.py +149 -7
  27. agent_folder_workspace-0.1.2/tests/test_mcp_pdf_survival.py +195 -0
  28. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/test_parser_workers.py +1 -0
  29. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/test_search_cache.py +43 -0
  30. agent_folder_workspace-0.1.2/tests/test_workspace_manager.py +165 -0
  31. agent_folder_workspace-0.1.0/src/agent_folder_workspace/cli.py +0 -139
  32. agent_folder_workspace-0.1.0/src/agent_folder_workspace/mcp_server.py +0 -179
  33. agent_folder_workspace-0.1.0/src/agent_folder_workspace/opencode/skill/SKILL.md +0 -24
  34. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/.github/workflows/ci.yml +0 -0
  35. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/.gitignore +0 -0
  36. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/CONTRIBUTING.md +0 -0
  37. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/LICENSE +0 -0
  38. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/SECURITY.md +0 -0
  39. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/docs/acceptance.md +0 -0
  40. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/docs/formats.md +0 -0
  41. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/scripts/generate_json_schemas.py +0 -0
  42. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/scripts/generate_probe_fixtures.py +0 -0
  43. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/__init__.py +0 -0
  44. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/base.py +0 -0
  45. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/legacy_office.py +0 -0
  46. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/manager.py +0 -0
  47. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/odf.py +0 -0
  48. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/ooxml.py +0 -0
  49. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/pdf.py +0 -0
  50. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/sqlite.py +0 -0
  51. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/structured.py +0 -0
  52. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/text.py +0 -0
  53. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/alternative_backends.py +0 -0
  54. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/backend_conversion_worker.py +0 -0
  55. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/backend_probe_worker.py +0 -0
  56. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/backends.py +0 -0
  57. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/cache.py +0 -0
  58. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/cfb.py +0 -0
  59. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/config.py +0 -0
  60. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/cursors.py +0 -0
  61. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/errors.py +0 -0
  62. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/ids.py +0 -0
  63. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/installer.py +0 -0
  64. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/legacy.py +0 -0
  65. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/opencode/plugin/agent-folder-workspace.ts.tmpl +0 -0
  66. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/parser_workers.py +0 -0
  67. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/path_safety.py +0 -0
  68. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/probe_fixtures/probe.doc +0 -0
  69. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/probe_fixtures/probe.ppt +0 -0
  70. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/probe_fixtures/probe.xls +0 -0
  71. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/py.typed +0 -0
  72. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/registry.py +0 -0
  73. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/schemas/CapabilityReportV1.schema.json +0 -0
  74. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/schemas/ContentPageV1.schema.json +0 -0
  75. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/schemas/DiagnosticV1.schema.json +0 -0
  76. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/schemas/NodePageV1.schema.json +0 -0
  77. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/schemas/NodeV1.schema.json +0 -0
  78. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/schemas/SearchPageV1.schema.json +0 -0
  79. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/__init__.py +0 -0
  80. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/cfb_fixture.py +0 -0
  81. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/test_alternative_backends.py +0 -0
  82. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/test_backends.py +0 -0
  83. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/test_cfb.py +0 -0
  84. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/test_content_adapters.py +0 -0
  85. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/test_legacy_adapters.py +0 -0
  86. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/test_legacy_fuzz.py +0 -0
  87. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/test_legacy_parsers.py +0 -0
  88. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/test_odf_pdf_adapters.py +0 -0
  89. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/test_ooxml_adapters.py +0 -0
  90. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/test_package.py +0 -0
  91. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/test_structured_text.py +0 -0
  92. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/test_windows_reparse_security.py +0 -0
  93. {agent_folder_workspace-0.1.0 → agent_folder_workspace-0.1.2}/tests/test_workspace_registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-folder-workspace
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Offline, lazy, agent-oriented exploration of heterogeneous folders
5
5
  Author-email: Dark Light <darklight@noreply.com>
6
6
  License-Expression: Apache-2.0
@@ -32,12 +32,12 @@ Description-Content-Type: text/markdown
32
32
 
33
33
  # agent-folder-workspace
34
34
 
35
- `agent-folder-workspace` presents one local directory as a stable, paginated
36
- node hierarchy for agents. It discovers file metadata first and opens document
35
+ `agent-folder-workspace` presents local directories as stable, paginated node
36
+ hierarchies for agents. It discovers file metadata first and opens document
37
37
  bodies only when a caller expands or reads a node. The default parser path is
38
38
  local Python code: Microsoft Office and LibreOffice are not required.
39
39
 
40
- > **Project status:** `0.1.0` is an alpha contract. Read-only exploration and
40
+ > **Project status:** `0.1.2` is an alpha contract. Read-only exploration and
41
41
  > byte access are implemented; editing, rendering, OCR, and complete semantic
42
42
  > coverage of every supported container are not.
43
43
 
@@ -92,7 +92,9 @@ adapters, and caching:
92
92
  adapters and enforce one deadline across startup, queueing, IPC, and parsing.
93
93
  5. `SizedLru` retains bounded decoded pages in memory. `ContentIndex` stores an
94
94
  optional per-workspace SQLite search index in the application cache.
95
- 6. The Python API, Typer CLI, and FastMCP stdio server share the same workspace
95
+ 6. `WorkspaceManager` gives one FastMCP process a bounded registry of active and
96
+ explicitly addressed workspace roots.
97
+ 7. The Python API, Typer CLI, and FastMCP stdio server share the same workspace
96
98
  methods and versioned response contracts.
97
99
 
98
100
  Node IDs are deterministic hashes of relative and logical paths; they do not
@@ -131,7 +133,7 @@ archive, XML, and structural limits. It is frozen and rejects unknown fields.
131
133
  The package exports a stable
132
134
  error hierarchy (`FolderWorkspaceError`, `InputValidationError`,
133
135
  `NodeNotFoundError`, `StaleCursorError`, and `ResourceLimitError`) and six
134
- versioned wire models:
136
+ document wire models plus three workspace-selection models:
135
137
 
136
138
  - `CapabilityReportV1`
137
139
  - `ContentPageV1`
@@ -139,6 +141,9 @@ versioned wire models:
139
141
  - `NodePageV1`
140
142
  - `NodeV1`
141
143
  - `SearchPageV1`
144
+ - `WorkspaceCloseV1`
145
+ - `WorkspaceHandleV1`
146
+ - `WorkspaceListV1`
142
147
 
143
148
  Committed JSON Schemas live in
144
149
  `src/agent_folder_workspace/schemas/`. Regenerate them with
@@ -147,7 +152,7 @@ Pydantic's `model_json_schema()` output.
147
152
 
148
153
  ## MCP server
149
154
 
150
- Run one stdio server for one root:
155
+ Run one stdio server with an initial root:
151
156
 
152
157
  ```console
153
158
  folderws mcp --root ./documents
@@ -155,6 +160,10 @@ folderws mcp --root ./documents
155
160
 
156
161
  The server exposes exactly these tools:
157
162
 
163
+ - `open_workspace`
164
+ - `list_workspaces`
165
+ - `activate_workspace`
166
+ - `close_workspace`
158
167
  - `workspace_info`
159
168
  - `list_children`
160
169
  - `get_node`
@@ -165,16 +174,40 @@ The server exposes exactly these tools:
165
174
  - `refresh_workspace`
166
175
  - `probe_backends`
167
176
 
177
+ `open_workspace(path)` accepts an explicit absolute directory, returns an opaque,
178
+ process-lifetime `workspace_id`, and makes that workspace active. Existing tools
179
+ accept an optional `workspace_id`; omission uses the active workspace. Opening
180
+ the same canonical root again reuses its ID. `list_workspaces`,
181
+ `activate_workspace`, and `close_workspace` manage the registry. One process
182
+ holds at most eight workspaces and never evicts one silently.
183
+
184
+ On Windows, runtime selection accepts `C:\Documents`, `C:/Documents`, UNC paths,
185
+ and Git Bash paths such as `/c/Documents`. Relative external paths are rejected.
186
+ The initial CLI root remains compatible with relative paths such as `.`.
187
+
168
188
  Results use the same versioned models as the Python API. A method that is absent
169
189
  from an injected workspace implementation returns a structured
170
190
  `capability_unavailable` response instead of failing server construction.
191
+ Expected workspace/parser failures and unexpected tool exceptions become
192
+ structured results; a later MCP request remains available. PDF decoding remains
193
+ lazy and isolated in bounded parser workers.
171
194
 
172
195
  The transport is stdio. There is no HTTP listener, authentication layer, or
173
- multi-root broker. The process and its MCP client share the operating-system
174
- permissions of the account that launched `folderws`; choose the root narrowly.
196
+ network broker. The process and its MCP client share the operating-system
197
+ permissions of the account that launched `folderws`; open roots narrowly.
175
198
 
176
199
  ## CLI
177
200
 
201
+ All commands accept global options before the subcommand:
202
+ `folderws [--quiet | --verbose] [--log-file PATH] COMMAND ...`. Human status,
203
+ progress, warnings, and errors are written to stderr; the JSON reports from
204
+ `doctor`, `index`, `clear-cache`, and `install-opencode` remain unadorned on
205
+ stdout. MCP keeps stdout exclusively for JSON-RPC. By default, durable UTF-8
206
+ logs are written to `platformdirs`' user log directory as `folderws.log`, with
207
+ 5 MiB rotation and three backups. An explicit log file must not be a symbolic
208
+ link, junction, or reparse-point destination; ordinary log permission failures
209
+ fall back to stderr-only operation.
210
+
178
211
  ```console
179
212
  # Serve a folder to an MCP client over stdio
180
213
  folderws mcp --root DIR
@@ -289,6 +322,8 @@ The normal Python parser path is offline and read-only. It does not upload
289
322
  documents and does not execute document macros. Important controls include:
290
323
 
291
324
  - roots that are symbolic links or reparse points are rejected;
325
+ - external roots are opened only by an explicit `open_workspace` call, are
326
+ bounded to eight per process, and receive process-lifetime opaque IDs;
292
327
  - child links are not followed by default, and resolved sources must remain
293
328
  under the selected root;
294
329
  - SQLite files are opened with `mode=ro` and `PRAGMA query_only=ON`;
@@ -375,7 +410,7 @@ The detailed coverage and per-format caveats are in
375
410
  rendering. A hard parent timeout cannot guarantee cleanup of a vendor process
376
411
  that is itself stuck below COM or LibreOffice process control.
377
412
  - The stated 100,000-node and warm p95 performance figures are design targets,
378
- not certified guarantees in `0.1.0`; CI covers correctness across Windows,
413
+ not certified guarantees in `0.1.2`; CI covers correctness across Windows,
379
414
  Linux, and macOS but does not emulate a specific Windows-x64 SSD workload.
380
415
 
381
416
  ## Development
@@ -1,11 +1,11 @@
1
1
  # agent-folder-workspace
2
2
 
3
- `agent-folder-workspace` presents one local directory as a stable, paginated
4
- node hierarchy for agents. It discovers file metadata first and opens document
3
+ `agent-folder-workspace` presents local directories as stable, paginated node
4
+ hierarchies for agents. It discovers file metadata first and opens document
5
5
  bodies only when a caller expands or reads a node. The default parser path is
6
6
  local Python code: Microsoft Office and LibreOffice are not required.
7
7
 
8
- > **Project status:** `0.1.0` is an alpha contract. Read-only exploration and
8
+ > **Project status:** `0.1.2` is an alpha contract. Read-only exploration and
9
9
  > byte access are implemented; editing, rendering, OCR, and complete semantic
10
10
  > coverage of every supported container are not.
11
11
 
@@ -60,7 +60,9 @@ adapters, and caching:
60
60
  adapters and enforce one deadline across startup, queueing, IPC, and parsing.
61
61
  5. `SizedLru` retains bounded decoded pages in memory. `ContentIndex` stores an
62
62
  optional per-workspace SQLite search index in the application cache.
63
- 6. The Python API, Typer CLI, and FastMCP stdio server share the same workspace
63
+ 6. `WorkspaceManager` gives one FastMCP process a bounded registry of active and
64
+ explicitly addressed workspace roots.
65
+ 7. The Python API, Typer CLI, and FastMCP stdio server share the same workspace
64
66
  methods and versioned response contracts.
65
67
 
66
68
  Node IDs are deterministic hashes of relative and logical paths; they do not
@@ -99,7 +101,7 @@ archive, XML, and structural limits. It is frozen and rejects unknown fields.
99
101
  The package exports a stable
100
102
  error hierarchy (`FolderWorkspaceError`, `InputValidationError`,
101
103
  `NodeNotFoundError`, `StaleCursorError`, and `ResourceLimitError`) and six
102
- versioned wire models:
104
+ document wire models plus three workspace-selection models:
103
105
 
104
106
  - `CapabilityReportV1`
105
107
  - `ContentPageV1`
@@ -107,6 +109,9 @@ versioned wire models:
107
109
  - `NodePageV1`
108
110
  - `NodeV1`
109
111
  - `SearchPageV1`
112
+ - `WorkspaceCloseV1`
113
+ - `WorkspaceHandleV1`
114
+ - `WorkspaceListV1`
110
115
 
111
116
  Committed JSON Schemas live in
112
117
  `src/agent_folder_workspace/schemas/`. Regenerate them with
@@ -115,7 +120,7 @@ Pydantic's `model_json_schema()` output.
115
120
 
116
121
  ## MCP server
117
122
 
118
- Run one stdio server for one root:
123
+ Run one stdio server with an initial root:
119
124
 
120
125
  ```console
121
126
  folderws mcp --root ./documents
@@ -123,6 +128,10 @@ folderws mcp --root ./documents
123
128
 
124
129
  The server exposes exactly these tools:
125
130
 
131
+ - `open_workspace`
132
+ - `list_workspaces`
133
+ - `activate_workspace`
134
+ - `close_workspace`
126
135
  - `workspace_info`
127
136
  - `list_children`
128
137
  - `get_node`
@@ -133,16 +142,40 @@ The server exposes exactly these tools:
133
142
  - `refresh_workspace`
134
143
  - `probe_backends`
135
144
 
145
+ `open_workspace(path)` accepts an explicit absolute directory, returns an opaque,
146
+ process-lifetime `workspace_id`, and makes that workspace active. Existing tools
147
+ accept an optional `workspace_id`; omission uses the active workspace. Opening
148
+ the same canonical root again reuses its ID. `list_workspaces`,
149
+ `activate_workspace`, and `close_workspace` manage the registry. One process
150
+ holds at most eight workspaces and never evicts one silently.
151
+
152
+ On Windows, runtime selection accepts `C:\Documents`, `C:/Documents`, UNC paths,
153
+ and Git Bash paths such as `/c/Documents`. Relative external paths are rejected.
154
+ The initial CLI root remains compatible with relative paths such as `.`.
155
+
136
156
  Results use the same versioned models as the Python API. A method that is absent
137
157
  from an injected workspace implementation returns a structured
138
158
  `capability_unavailable` response instead of failing server construction.
159
+ Expected workspace/parser failures and unexpected tool exceptions become
160
+ structured results; a later MCP request remains available. PDF decoding remains
161
+ lazy and isolated in bounded parser workers.
139
162
 
140
163
  The transport is stdio. There is no HTTP listener, authentication layer, or
141
- multi-root broker. The process and its MCP client share the operating-system
142
- permissions of the account that launched `folderws`; choose the root narrowly.
164
+ network broker. The process and its MCP client share the operating-system
165
+ permissions of the account that launched `folderws`; open roots narrowly.
143
166
 
144
167
  ## CLI
145
168
 
169
+ All commands accept global options before the subcommand:
170
+ `folderws [--quiet | --verbose] [--log-file PATH] COMMAND ...`. Human status,
171
+ progress, warnings, and errors are written to stderr; the JSON reports from
172
+ `doctor`, `index`, `clear-cache`, and `install-opencode` remain unadorned on
173
+ stdout. MCP keeps stdout exclusively for JSON-RPC. By default, durable UTF-8
174
+ logs are written to `platformdirs`' user log directory as `folderws.log`, with
175
+ 5 MiB rotation and three backups. An explicit log file must not be a symbolic
176
+ link, junction, or reparse-point destination; ordinary log permission failures
177
+ fall back to stderr-only operation.
178
+
146
179
  ```console
147
180
  # Serve a folder to an MCP client over stdio
148
181
  folderws mcp --root DIR
@@ -257,6 +290,8 @@ The normal Python parser path is offline and read-only. It does not upload
257
290
  documents and does not execute document macros. Important controls include:
258
291
 
259
292
  - roots that are symbolic links or reparse points are rejected;
293
+ - external roots are opened only by an explicit `open_workspace` call, are
294
+ bounded to eight per process, and receive process-lifetime opaque IDs;
260
295
  - child links are not followed by default, and resolved sources must remain
261
296
  under the selected root;
262
297
  - SQLite files are opened with `mode=ro` and `PRAGMA query_only=ON`;
@@ -343,7 +378,7 @@ The detailed coverage and per-format caveats are in
343
378
  rendering. A hard parent timeout cannot guarantee cleanup of a vendor process
344
379
  that is itself stuck below COM or LibreOffice process control.
345
380
  - The stated 100,000-node and warm p95 performance figures are design targets,
346
- not certified guarantees in `0.1.0`; CI covers correctness across Windows,
381
+ not certified guarantees in `0.1.2`; CI covers correctness across Windows,
347
382
  Linux, and macOS but does not emulate a specific Windows-x64 SSD workload.
348
383
 
349
384
  ## Development
@@ -7,7 +7,8 @@ tests, not this prose alone, define the wire contract.
7
7
 
8
8
  ```mermaid
9
9
  flowchart LR
10
- Client["Python, CLI, or MCP client"] --> Workspace["FolderWorkspace"]
10
+ Client["Python, CLI, or MCP client"] --> Session["WorkspaceManager"]
11
+ Session --> Workspace["FolderWorkspace"]
11
12
  Workspace --> Registry["NodeRegistry metadata tree"]
12
13
  Workspace --> Manager["AdapterManager"]
13
14
  Manager --> Direct["Direct text, CSV, and binary paths"]
@@ -31,7 +32,7 @@ is an explicit diagnostic path.
31
32
 
32
33
  `models.py` contains strict Pydantic models. Wire-level models carry
33
34
  `schema_version="1.0"`; unknown fields are rejected and non-finite numbers are
34
- not accepted. The six top-level exported models have committed JSON Schemas in
35
+ not accepted. The nine top-level exported models have committed JSON Schemas in
35
36
  `src/agent_folder_workspace/schemas/`.
36
37
 
37
38
  `errors.py` provides stable exception categories, and `ids.py` creates node IDs
@@ -192,9 +193,18 @@ copies.
192
193
 
193
194
  The Python API returns model instances and raises the public error hierarchy.
194
195
  The FastMCP server serializes model responses with `model_dump(mode="json")`
195
- and exposes a fixed nine-tool stdio surface. Its capability dispatcher resolves
196
- workspace methods at call time so reduced injected implementations return a
197
- structured `capability_unavailable` error.
196
+ and exposes a fixed thirteen-tool stdio surface. `WorkspaceManager` owns at
197
+ most eight roots, opaque process-lifetime IDs, canonical-root deduplication,
198
+ active selection, and deterministic cleanup. Existing tools accept an optional
199
+ workspace ID and otherwise route to the active root. Its capability dispatcher
200
+ resolves workspace methods at call time so reduced injected implementations
201
+ return a structured `capability_unavailable` error.
202
+
203
+ MCP tool boundaries translate public workspace errors to stable error codes and
204
+ sanitize unexpected failures behind a correlation ID. This leaves the stdio
205
+ transport usable for a later request. Risky PDF and container semantics remain
206
+ inside parser workers; opening or listing a filesystem root does not decode
207
+ their payloads.
198
208
 
199
209
  The Typer CLI provides MCP serving, backend diagnostics, indexing, cache
200
210
  clearing, and managed OpenCode integration installation. OpenCode resources are
@@ -218,8 +228,10 @@ enterprise policy, add-in, crash, or vendor defect.
218
228
 
219
229
  ## Security boundaries
220
230
 
221
- - The selected root is the filesystem authority boundary; the library does not
222
- implement per-node authorization.
231
+ - Every selected root is a separate filesystem authority boundary; the library
232
+ does not implement per-node authorization.
233
+ - Runtime roots require an explicit absolute path. Windows native, slash-style,
234
+ UNC, and Git Bash drive paths normalize before registry deduplication.
223
235
  - The default MCP transport is local stdio; operating-system permissions govern
224
236
  the process and client.
225
237
  - `read_binary` intentionally reveals file bytes inside the root.
@@ -0,0 +1,270 @@
1
+ # Console Logging Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Add durable, safe, cross-platform console and file logging to `folderws` without changing JSON stdout or MCP stdio contracts.
6
+
7
+ **Architecture:** A focused `console_logging.py` module owns handler setup, safe log-path selection, redaction, lifecycle context, and throttled progress. The Typer root callback initializes it once, command functions emit lifecycle events, and a `main()` entrypoint translates package and unexpected exceptions into stable exits while preserving Click/Typer behavior. `FolderWorkspace.index_content()` exposes an optional callback so the CLI can report bounded progress without affecting Python or MCP callers.
8
+
9
+ **Tech Stack:** Python 3.11+, stdlib `logging`/`RotatingFileHandler`, Typer/Click, platformdirs, pytest, Ruff, strict Mypy, Hatchling.
10
+
11
+ ---
12
+
13
+ ### Task 1: Implement and verify the complete logging contract
14
+
15
+ **Files:**
16
+ - Create: `src/agent_folder_workspace/console_logging.py`
17
+ - Create: `tests/test_console_logging.py`
18
+ - Modify: `src/agent_folder_workspace/cli.py`
19
+ - Modify: `src/agent_folder_workspace/__main__.py`
20
+ - Modify: `src/agent_folder_workspace/workspace.py:1010`
21
+ - Modify: `pyproject.toml:41`
22
+ - Modify: `README.md:145`
23
+ - Modify: `docs/windows.md:20`
24
+
25
+ - [ ] **Step 1: Write failing unit tests for logging setup, path safety, rotation, and redaction**
26
+
27
+ Add tests which import the wished-for API below, configure it against `tmp_path`, write records, flush handlers, and assert that secrets are absent from both console and file output:
28
+
29
+ ```python
30
+ from agent_folder_workspace.console_logging import (
31
+ LoggingConfigurationError,
32
+ configure_logging,
33
+ default_log_file,
34
+ redact_text,
35
+ shutdown_logging,
36
+ )
37
+
38
+ def test_redact_text_hides_known_secret_shapes() -> None:
39
+ value = "api-key=secret OPENAI_API_KEY=sk-test Authorization: Bearer token"
40
+ redacted = redact_text(value)
41
+ assert "secret" not in redacted
42
+ assert "sk-test" not in redacted
43
+ assert "token" not in redacted
44
+
45
+ def test_configure_logging_writes_human_console_to_stderr_and_debug_to_file(
46
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
47
+ ) -> None:
48
+ state = configure_logging(log_file=tmp_path / "folderws.log")
49
+ logger = logging.getLogger("agent_folder_workspace.test")
50
+ logger.info("visible status")
51
+ logger.debug("durable detail")
52
+ shutdown_logging(state)
53
+ captured = capsys.readouterr()
54
+ assert captured.out == ""
55
+ assert "visible status" in captured.err
56
+ assert "durable detail" in (tmp_path / "folderws.log").read_text(encoding="utf-8")
57
+ ```
58
+
59
+ Cover default path resolution via a monkeypatched platform log directory, `quiet` showing only errors, `verbose` showing debug, three 5 MiB rotating backups, a linked/reparse terminal or parent raising `LoggingConfigurationError`, an unwritable file falling back without aborting, and Windows-style lexical path text not crashing formatting. Keep each test isolated by calling `shutdown_logging()`.
60
+
61
+ - [ ] **Step 2: Run the new logging tests and verify RED**
62
+
63
+ Run: `pytest tests/test_console_logging.py -v`
64
+
65
+ Expected: collection fails because `agent_folder_workspace.console_logging` does not exist.
66
+
67
+ - [ ] **Step 3: Implement the focused logging module minimally**
68
+
69
+ Implement these typed public interfaces and private helpers in `console_logging.py`:
70
+
71
+ ```python
72
+ @dataclass(frozen=True)
73
+ class LoggingState:
74
+ correlation_id: str
75
+ log_file: Path | None
76
+ handlers: tuple[logging.Handler, ...]
77
+
78
+ class LoggingConfigurationError(ValueError):
79
+ pass
80
+
81
+ def default_log_file() -> Path:
82
+ return Path(user_log_dir("agent-folder-workspace", appauthor=False)) / "folderws.log"
83
+
84
+ def redact_text(value: object) -> str: ...
85
+
86
+ def configure_logging(
87
+ *, quiet: bool = False, verbose: bool = False, log_file: Path | None = None
88
+ ) -> LoggingState: ...
89
+
90
+ def shutdown_logging(state: LoggingState | None = None) -> None: ...
91
+ ```
92
+
93
+ Use a package-root logger with propagation disabled, a stderr `StreamHandler`, and a UTF-8 `RotatingFileHandler(maxBytes=5 * 1024 * 1024, backupCount=3)`. File level is DEBUG; console level is ERROR for quiet, DEBUG for verbose, INFO otherwise. Refuse any destination for which `first_unsafe_component()` returns a component. Create parent directories only after validation, revalidate before opening, and catch ordinary file-handler setup failures so console logging remains available with one warning. Redact bearer tokens and values associated with case-insensitive names containing `api_key`, `apikey`, `token`, `secret`, `password`, or `authorization`. Formatting and redaction must fall back to safe type names instead of raising.
94
+
95
+ - [ ] **Step 4: Run logging tests and verify GREEN**
96
+
97
+ Run: `pytest tests/test_console_logging.py -v`
98
+
99
+ Expected: all tests pass.
100
+
101
+ - [ ] **Step 5: Write failing tests for deterministic workspace index progress**
102
+
103
+ In `tests/test_search_cache.py`, create a workspace with two readable files and one adapter failure, then call the wished-for callback contract:
104
+
105
+ ```python
106
+ events: list[tuple[int, int, int]] = []
107
+ info = workspace.index_content(progress=lambda done, total, failed: events.append((done, total, failed)))
108
+ assert events[-1] == (3, 3, 1)
109
+ assert [event[0] for event in events] == [1, 2, 3]
110
+ assert info.total_files == 3
111
+ ```
112
+
113
+ Also assert that `workspace.index_content()` with no callback remains valid.
114
+
115
+ - [ ] **Step 6: Run the progress tests and verify RED**
116
+
117
+ Run: `pytest tests/test_search_cache.py -v`
118
+
119
+ Expected: the callback test fails because `index_content()` does not accept `progress`.
120
+
121
+ - [ ] **Step 7: Add the optional progress callback to the workspace API**
122
+
123
+ Change the signature and loop without changing exception swallowing:
124
+
125
+ ```python
126
+ def index_content(
127
+ self,
128
+ *,
129
+ progress: Callable[[int, int, int], None] | None = None,
130
+ ) -> WorkspaceInfoV1:
131
+ files = self._file_nodes()
132
+ failed = 0
133
+ for completed, file_node in enumerate(files, start=1):
134
+ index.begin_file(file_node)
135
+ try:
136
+ self._index_file(file_node, index)
137
+ except (FolderWorkspaceError, OSError, ValueError):
138
+ failed += 1
139
+ else:
140
+ index.finish_file(file_node.id)
141
+ if progress is not None:
142
+ progress(completed, len(files), failed)
143
+ return self.workspace_info()
144
+ ```
145
+
146
+ Import `Callable` from `collections.abc`. Never invoke the callback with paths or content.
147
+
148
+ - [ ] **Step 8: Run the progress tests and verify GREEN**
149
+
150
+ Run: `pytest tests/test_search_cache.py -v`
151
+
152
+ Expected: all progress and existing search/cache tests pass.
153
+
154
+ - [ ] **Step 9: Write failing CLI contract tests before changing CLI code**
155
+
156
+ Extend `tests/test_console_logging.py` and `tests/test_mcp_cli_installer.py` to prove:
157
+
158
+ ```python
159
+ result = CliRunner().invoke(app, ["--log-file", str(log_file), "doctor"])
160
+ assert result.exit_code == 0
161
+ json.loads(result.stdout)
162
+ assert "command=doctor" in result.stderr
163
+
164
+ conflict = CliRunner().invoke(app, ["--quiet", "--verbose", "doctor"])
165
+ assert conflict.exit_code == 2
166
+
167
+ quiet = CliRunner().invoke(app, ["--quiet", "--log-file", str(log_file), "doctor"])
168
+ assert quiet.stderr == ""
169
+ json.loads(quiet.stdout)
170
+ ```
171
+
172
+ Add tests for command start/completion and elapsed time, selected roots/targets, install conflict exit 2, known `FolderWorkspaceError` declared exit code, an `OSError` exit 1 with concise stderr, unexpected failure exit 1 with a correlation ID and no traceback on stderr but a traceback in the file, and throttled `index` progress including final counts. Assert secrets passed in exception messages are redacted. Exercise `main()` directly for translated errors.
173
+
174
+ For MCP purity, run the command with a monkeypatched `run_mcp` that writes a JSON-RPC line to stdout and logs an INFO record; assert stdout contains only the JSON-RPC line and the human record is on stderr. Assert that Click usage errors and explicit `typer.Exit` retain native exit codes.
175
+
176
+ - [ ] **Step 10: Run CLI tests and verify RED**
177
+
178
+ Run: `pytest tests/test_console_logging.py tests/test_mcp_cli_installer.py -v`
179
+
180
+ Expected: failures show missing global options, lifecycle output, entrypoint wrapper, and CLI progress wiring.
181
+
182
+ - [ ] **Step 11: Integrate logging into Typer and the package entrypoints**
183
+
184
+ Add a root callback with `--quiet`, `--verbose`, and `--log-file`; reject the option conflict with `typer.BadParameter`. Store `LoggingState` in `typer.Context.obj`. Add a lifecycle context manager and throttled progress reporter in `console_logging.py` with these interfaces:
185
+
186
+ ```python
187
+ @contextmanager
188
+ def command_lifecycle(command: str, **safe_fields: object) -> Iterator[None]: ...
189
+
190
+ class IndexProgressReporter:
191
+ def __call__(self, completed: int, total: int, failed: int) -> None: ...
192
+ ```
193
+
194
+ The reporter emits the first event, final event, failures, and bounded intermediate events (at most once per second or every 100 files), while unit tests can inject a clock and interval. Wrap each command body in `command_lifecycle`, logging only safe paths/counts/flags. For `index`, call `workspace.index_content(progress=IndexProgressReporter(...))` directly and preserve the stable JSON result using `_jsonable` behavior; keep late capability handling if the method is unavailable.
195
+
196
+ Implement:
197
+
198
+ ```python
199
+ def main() -> None:
200
+ try:
201
+ app(standalone_mode=False)
202
+ except (click.UsageError, click.Abort):
203
+ raise
204
+ except click.exceptions.Exit as error:
205
+ raise SystemExit(error.exit_code) from error
206
+ except FolderWorkspaceError as error:
207
+ log_expected_failure(error)
208
+ raise SystemExit(error.exit_code) from error
209
+ except (OSError, subprocess.SubprocessError) as error:
210
+ log_expected_failure(error)
211
+ raise SystemExit(1) from error
212
+ except Exception as error:
213
+ log_unexpected_failure(error)
214
+ raise SystemExit(1) from error
215
+ finally:
216
+ shutdown_logging()
217
+ ```
218
+
219
+ Adapt exception handling as required by actual Typer behavior, but preserve native usage/help and explicit exit behavior. Update `pyproject.toml` to `folderws = "agent_folder_workspace.cli:main"` and `__main__.py` to call `main()`. Ensure unexpected tracebacks use only the file handler; stderr gets a concise redacted correlation ID and log path.
220
+
221
+ - [ ] **Step 12: Run CLI tests and verify GREEN**
222
+
223
+ Run: `pytest tests/test_console_logging.py tests/test_mcp_cli_installer.py -v`
224
+
225
+ Expected: all tests pass and JSON/MCP stdout assertions remain exact.
226
+
227
+ - [ ] **Step 13: Document the global logging behavior**
228
+
229
+ Add the following command synopsis and operational notes to README and Windows docs:
230
+
231
+ ```console
232
+ folderws [--quiet | --verbose] [--log-file PATH] COMMAND [ARGS]...
233
+ ```
234
+
235
+ State that human output is stderr, JSON/MCP remains stdout, default logs use the platform user log directory with three 5 MiB backups, `--quiet` retains errors, `--verbose` adds console diagnostics, and a custom log path must not traverse symlinks/junctions/reparse points. Include PowerShell and Git Bash examples with paths containing spaces.
236
+
237
+ - [ ] **Step 14: Run focused static and formatting gates**
238
+
239
+ Run:
240
+
241
+ ```bash
242
+ ruff format src/agent_folder_workspace/console_logging.py src/agent_folder_workspace/cli.py src/agent_folder_workspace/workspace.py tests/test_console_logging.py tests/test_mcp_cli_installer.py tests/test_search_cache.py
243
+ ruff check src/agent_folder_workspace/console_logging.py src/agent_folder_workspace/cli.py src/agent_folder_workspace/workspace.py tests/test_console_logging.py tests/test_mcp_cli_installer.py tests/test_search_cache.py
244
+ mypy --no-incremental
245
+ ```
246
+
247
+ Expected: formatting makes no further changes on a second check, Ruff reports no errors, and Mypy reports success.
248
+
249
+ - [ ] **Step 15: Run all release gates and installed-wheel smoke tests**
250
+
251
+ Run:
252
+
253
+ ```bash
254
+ python scripts/generate_json_schemas.py --check
255
+ ruff format --check src scripts tests
256
+ ruff check --no-cache .
257
+ mypy --no-incremental
258
+ pytest
259
+ rm -rf dist
260
+ python -m build --no-isolation
261
+ python -m twine check dist/*
262
+ python scripts/check_wheel.py dist --install-smoke
263
+ python scripts/check_wheel.py --reproducible
264
+ ```
265
+
266
+ Expected: every command exits 0; the fresh-wheel CLI smoke confirms `folderws --help`, JSON stdout, stderr logging, explicit `--log-file`, and MCP importability. If Twine is not installed, use an isolated temporary environment to install it rather than adding it as a runtime dependency.
267
+
268
+ - [ ] **Step 16: Commit the implementation**
269
+
270
+ Stage only the files listed above and commit through the repository's required commit-policy helper with a title such as `Add durable folder workspace logging` and bullets covering protocol-safe stderr/file logging, progress/error diagnostics, and verification coverage.