imap-agent-cli 0.1.6__tar.gz → 0.2.0__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 (36) hide show
  1. imap_agent_cli-0.2.0/CHANGELOG.md +10 -0
  2. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/PKG-INFO +45 -9
  3. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/README.md +41 -7
  4. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/imap_agent_cli.py +2 -0
  5. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/pyproject.toml +3 -1
  6. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/spec.md +20 -0
  7. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/src/imap_agent_cli/__init__.py +1 -1
  8. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/src/imap_agent_cli/cli.py +60 -13
  9. imap_agent_cli-0.2.0/src/imap_agent_cli/runtime.py +41 -0
  10. imap_agent_cli-0.2.0/src/imap_agent_cli/skill.py +476 -0
  11. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/tests/_bootstrap.py +3 -2
  12. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/tests/test_cli.py +5 -0
  13. imap_agent_cli-0.2.0/tests/test_runtime.py +61 -0
  14. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/tests/test_skill.py +7 -1
  15. imap_agent_cli-0.2.0/tests/test_skill_sync.py +470 -0
  16. imap_agent_cli-0.2.0/tests/test_wheel_smoke.py +101 -0
  17. imap_agent_cli-0.1.6/src/imap_agent_cli/skill.py +0 -219
  18. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/.github/workflows/publish.yml +0 -0
  19. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/.gitignore +0 -0
  20. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/AGENTS.md +0 -0
  21. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/LICENSE +0 -0
  22. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/src/imap_agent_cli/config.py +0 -0
  23. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/src/imap_agent_cli/errors.py +0 -0
  24. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/src/imap_agent_cli/imap_client.py +0 -0
  25. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/src/imap_agent_cli/mime.py +0 -0
  26. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/src/imap_agent_cli/models.py +0 -0
  27. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/src/imap_agent_cli/render.py +0 -0
  28. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/src/imap_agent_cli/search.py +0 -0
  29. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/tests/__init__.py +0 -0
  30. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/tests/pymap_server_runner.py +0 -0
  31. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/tests/test_config.py +0 -0
  32. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/tests/test_imap_client.py +0 -0
  33. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/tests/test_live_no_seen.py +0 -0
  34. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/tests/test_mime.py +0 -0
  35. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/tests/test_pymap_integration.py +0 -0
  36. {imap_agent_cli-0.1.6 → imap_agent_cli-0.2.0}/tests/test_search.py +0 -0
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 - 2026-09-03
4
+
5
+ - Automatically synchronize existing managed `imap` skills in the standard directory to the running CLI's newer version. No package lookup, uv refresh, or CLI update is performed.
6
+ - Store management identity, exact CLI version, and normalized SHA-256 content integrity in `SKILL.md` YAML metadata. Migrate legacy markers and recover missing or malformed versions.
7
+ - Preserve modified or unverifiable content with valid versions. Add `uvx imap-agent-cli skill install --force` for explicit replacement of managed content.
8
+ - Add read-only `skill status` with JSON and plain output. Keep `install-skill`, `remove-skill`, custom directories, and removal-time `--force`.
9
+ - Skip automatic synchronization for local source and editable builds. Custom skills require explicit updates. Updates affect future agent skill loading.
10
+ - Replace skills atomically and preserve unrelated files during removal.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: imap-agent-cli
3
- Version: 0.1.6
3
+ Version: 0.2.0
4
4
  Summary: Agent-first IMAP CLI for safe email search, read, attachment download, and draft creation
5
5
  Project-URL: Homepage, https://github.com/pseudosavant/imap-agent-cli
6
6
  Project-URL: Repository, https://github.com/pseudosavant/imap-agent-cli
@@ -26,6 +26,8 @@ Requires-Dist: beautifulsoup4>=4.12.0
26
26
  Requires-Dist: bleach>=6.2.0
27
27
  Requires-Dist: imapclient>=3.0.0
28
28
  Requires-Dist: markdownify>=0.14.0
29
+ Requires-Dist: packaging>=23.2
30
+ Requires-Dist: pyyaml>=6.0
29
31
  Provides-Extra: test
30
32
  Requires-Dist: pymap>=0.36.0; extra == 'test'
31
33
  Requires-Dist: pytest>=8.0.0; extra == 'test'
@@ -44,7 +46,7 @@ It can inspect mailboxes and append messages to Drafts. It cannot send email, de
44
46
  Install or update the `imap` agent skill:
45
47
 
46
48
  ```text
47
- uvx imap-agent-cli install-skill
49
+ uvx imap-agent-cli skill install
48
50
  ```
49
51
 
50
52
  Configure the default IMAP account with environment variables:
@@ -86,7 +88,7 @@ python -m pip install imap-agent-cli
86
88
  imap-agent-cli --help
87
89
  ```
88
90
 
89
- All command payloads are JSON on stdout. Diagnostics and errors go to stderr.
91
+ Command payloads are JSON on stdout by default. Skill status also offers `--format plain`. Diagnostics and errors go to stderr.
90
92
 
91
93
  Use `imap-agent-cli --about` for project URL and license attribution. Use `imap-agent-cli --version` to print only the version number.
92
94
 
@@ -130,12 +132,12 @@ imap-agent-cli config check
130
132
 
131
133
  ### Agent Skill
132
134
 
133
- The installed skill teaches an agentic tool how to use `imap-agent-cli` safely and effectively.
135
+ The installed skill teaches an agentic tool how to use `imap-agent-cli` safely through `uvx`.
134
136
 
135
137
  Install or update the user-scoped `imap` skill:
136
138
 
137
139
  ```text
138
- uvx imap-agent-cli install-skill
140
+ uvx imap-agent-cli skill install
139
141
  ```
140
142
 
141
143
  This writes:
@@ -144,18 +146,44 @@ This writes:
144
146
  ~/.agents/skills/imap/SKILL.md
145
147
  ```
146
148
 
149
+ Normally installed CLI builds check this location on every ordinary invocation, including help, version, and about output. An already-installed managed skill from an older CLI version is replaced only if its recorded content hash still matches. Missing skills are never installed automatically. Unmanaged skills, equal versions, and newer versions are left alone.
150
+
151
+ The running CLI version is the authority. Synchronization is local. It does not query PyPI, refresh uv's cache, or update uv or the CLI. Updating those tools remains a separate action. Skill-management commands skip automatic synchronization.
152
+
153
+ Inspect the installed version, content integrity, and synchronization eligibility without changing the skill:
154
+
155
+ ```text
156
+ uvx imap-agent-cli skill status
157
+ uvx imap-agent-cli skill status --format plain
158
+ ```
159
+
160
+ Modified skills and skills with valid versions but missing or invalid hashes are preserved. To replace managed content explicitly:
161
+
162
+ ```text
163
+ uvx imap-agent-cli skill install --force
164
+ ```
165
+
166
+ Install-time `--force` still refuses unmanaged content and never downgrades a newer skill. Legacy skills with the old HTML managed marker migrate once as version 0. Missing or malformed managed versions receive a fresh replacement without a hash check. Ownership, version, and the SHA-256 content hash are stored in `SKILL.md` front matter under `metadata`. The hash detects edits. It is not a signature.
167
+
147
168
  Remove the managed skill:
148
169
 
149
170
  ```text
150
- uvx imap-agent-cli remove-skill
171
+ uvx imap-agent-cli skill remove
151
172
  ```
152
173
 
153
- Use `--skills-dir PATH` to install into a nonstandard skills directory:
174
+ Removal deletes only `SKILL.md` and removes its directory if empty. Unrelated files are kept. Removal retains `--force` for explicitly removing an unmanaged `SKILL.md`. The original `install-skill` and `remove-skill` aliases remain supported.
175
+
176
+ Use `--skills-dir PATH` on any skill command to select a custom skills root. Custom locations require explicit updates and are never discovered by ordinary invocations:
154
177
 
155
178
  ```text
156
- uvx imap-agent-cli install-skill --skills-dir ~/.agents/skills
179
+ uvx imap-agent-cli skill install --skills-dir ./my-skills
180
+ uvx imap-agent-cli skill status --skills-dir ./my-skills
157
181
  ```
158
182
 
183
+ Local checkouts, local source installs, and editable builds do not synchronize automatically. Unverifiable installation origins are also skipped. Explicit installation still works from development builds, including `uvx --from . imap-agent-cli skill install`. Installed wheels remain eligible, including wheels installed from a local file.
184
+
185
+ An update affects future agent skill loading. An agent session may retain instructions it has already loaded.
186
+
159
187
  ### Safety Boundary
160
188
 
161
189
  Allowed:
@@ -219,6 +247,14 @@ Run no-network unit tests:
219
247
  python -m unittest discover -v
220
248
  ```
221
249
 
250
+ To validate an installed wheel and local-source exclusions, build the package and set `IMAP_AGENT_CLI_WHEEL_TEST` to the wheel's path. Then run:
251
+
252
+ ```text
253
+ python -m unittest tests.test_wheel_smoke -v
254
+ ```
255
+
256
+ This opt-in check uses uv to install into a temporary environment with a temporary home directory. The regular tests also isolate skill paths from your home directory.
257
+
222
258
  Run the opt-in local IMAP integration test:
223
259
 
224
260
  Set `IMAP_AGENT_CLI_TEST_PYMAP=1` in your shell, then run:
@@ -11,7 +11,7 @@ It can inspect mailboxes and append messages to Drafts. It cannot send email, de
11
11
  Install or update the `imap` agent skill:
12
12
 
13
13
  ```text
14
- uvx imap-agent-cli install-skill
14
+ uvx imap-agent-cli skill install
15
15
  ```
16
16
 
17
17
  Configure the default IMAP account with environment variables:
@@ -53,7 +53,7 @@ python -m pip install imap-agent-cli
53
53
  imap-agent-cli --help
54
54
  ```
55
55
 
56
- All command payloads are JSON on stdout. Diagnostics and errors go to stderr.
56
+ Command payloads are JSON on stdout by default. Skill status also offers `--format plain`. Diagnostics and errors go to stderr.
57
57
 
58
58
  Use `imap-agent-cli --about` for project URL and license attribution. Use `imap-agent-cli --version` to print only the version number.
59
59
 
@@ -97,12 +97,12 @@ imap-agent-cli config check
97
97
 
98
98
  ### Agent Skill
99
99
 
100
- The installed skill teaches an agentic tool how to use `imap-agent-cli` safely and effectively.
100
+ The installed skill teaches an agentic tool how to use `imap-agent-cli` safely through `uvx`.
101
101
 
102
102
  Install or update the user-scoped `imap` skill:
103
103
 
104
104
  ```text
105
- uvx imap-agent-cli install-skill
105
+ uvx imap-agent-cli skill install
106
106
  ```
107
107
 
108
108
  This writes:
@@ -111,18 +111,44 @@ This writes:
111
111
  ~/.agents/skills/imap/SKILL.md
112
112
  ```
113
113
 
114
+ Normally installed CLI builds check this location on every ordinary invocation, including help, version, and about output. An already-installed managed skill from an older CLI version is replaced only if its recorded content hash still matches. Missing skills are never installed automatically. Unmanaged skills, equal versions, and newer versions are left alone.
115
+
116
+ The running CLI version is the authority. Synchronization is local. It does not query PyPI, refresh uv's cache, or update uv or the CLI. Updating those tools remains a separate action. Skill-management commands skip automatic synchronization.
117
+
118
+ Inspect the installed version, content integrity, and synchronization eligibility without changing the skill:
119
+
120
+ ```text
121
+ uvx imap-agent-cli skill status
122
+ uvx imap-agent-cli skill status --format plain
123
+ ```
124
+
125
+ Modified skills and skills with valid versions but missing or invalid hashes are preserved. To replace managed content explicitly:
126
+
127
+ ```text
128
+ uvx imap-agent-cli skill install --force
129
+ ```
130
+
131
+ Install-time `--force` still refuses unmanaged content and never downgrades a newer skill. Legacy skills with the old HTML managed marker migrate once as version 0. Missing or malformed managed versions receive a fresh replacement without a hash check. Ownership, version, and the SHA-256 content hash are stored in `SKILL.md` front matter under `metadata`. The hash detects edits. It is not a signature.
132
+
114
133
  Remove the managed skill:
115
134
 
116
135
  ```text
117
- uvx imap-agent-cli remove-skill
136
+ uvx imap-agent-cli skill remove
118
137
  ```
119
138
 
120
- Use `--skills-dir PATH` to install into a nonstandard skills directory:
139
+ Removal deletes only `SKILL.md` and removes its directory if empty. Unrelated files are kept. Removal retains `--force` for explicitly removing an unmanaged `SKILL.md`. The original `install-skill` and `remove-skill` aliases remain supported.
140
+
141
+ Use `--skills-dir PATH` on any skill command to select a custom skills root. Custom locations require explicit updates and are never discovered by ordinary invocations:
121
142
 
122
143
  ```text
123
- uvx imap-agent-cli install-skill --skills-dir ~/.agents/skills
144
+ uvx imap-agent-cli skill install --skills-dir ./my-skills
145
+ uvx imap-agent-cli skill status --skills-dir ./my-skills
124
146
  ```
125
147
 
148
+ Local checkouts, local source installs, and editable builds do not synchronize automatically. Unverifiable installation origins are also skipped. Explicit installation still works from development builds, including `uvx --from . imap-agent-cli skill install`. Installed wheels remain eligible, including wheels installed from a local file.
149
+
150
+ An update affects future agent skill loading. An agent session may retain instructions it has already loaded.
151
+
126
152
  ### Safety Boundary
127
153
 
128
154
  Allowed:
@@ -186,6 +212,14 @@ Run no-network unit tests:
186
212
  python -m unittest discover -v
187
213
  ```
188
214
 
215
+ To validate an installed wheel and local-source exclusions, build the package and set `IMAP_AGENT_CLI_WHEEL_TEST` to the wheel's path. Then run:
216
+
217
+ ```text
218
+ python -m unittest tests.test_wheel_smoke -v
219
+ ```
220
+
221
+ This opt-in check uses uv to install into a temporary environment with a temporary home directory. The regular tests also isolate skill paths from your home directory.
222
+
189
223
  Run the opt-in local IMAP integration test:
190
224
 
191
225
  Set `IMAP_AGENT_CLI_TEST_PYMAP=1` in your shell, then run:
@@ -5,6 +5,8 @@
5
5
  # "beautifulsoup4>=4.12.0",
6
6
  # "bleach>=6.2.0",
7
7
  # "markdownify>=0.14.0",
8
+ # "packaging>=23.2",
9
+ # "PyYAML>=6.0",
8
10
  # ]
9
11
  # ///
10
12
  """Local development wrapper for imap-agent-cli."""
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "imap-agent-cli"
7
- version = "0.1.6"
7
+ version = "0.2.0"
8
8
  description = "Agent-first IMAP CLI for safe email search, read, attachment download, and draft creation"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -32,6 +32,8 @@ dependencies = [
32
32
  "beautifulsoup4>=4.12.0",
33
33
  "bleach>=6.2.0",
34
34
  "markdownify>=0.14.0",
35
+ "packaging>=23.2",
36
+ "PyYAML>=6.0",
35
37
  ]
36
38
 
37
39
  [project.optional-dependencies]
@@ -319,6 +319,26 @@ imap-agent-cli config add-profile work --preset generic-ssl-993 --host colo-mail
319
319
 
320
320
  All commands output JSON to stdout by default. Diagnostics and errors go to stderr.
321
321
 
322
+ ## Managed Agent Skill
323
+
324
+ The distribution and command are `imap-agent-cli`. The Python import package is `imap_agent_cli`. The skill is `imap`, installed at `~/.agents/skills/imap/SKILL.md` by `uvx imap-agent-cli skill install`. The original `install-skill` and `remove-skill` commands remain aliases.
325
+
326
+ `imap_agent_cli.__version__` is the runtime authority and must match `--version`. Canonical skill generation writes `metadata.managed-by: imap-agent-cli`, a quoted `metadata.managed-version`, and a quoted `metadata.managed-content-sha256` with the `sha256:` prefix and 64 lowercase hexadecimal characters. There is no top-level version or sidecar file. The bundled instructions continue to prefer `uvx`.
327
+
328
+ The content hash covers the entire UTF-8 file after normalizing CRLF and CR to LF and replacing only the hash field's value with `""`. Verification uses the installed text without reserializing YAML. Generated files have LF endings, a trailing newline, and no byte order mark. This detects modifications and is not a signature.
329
+
330
+ Ordinary invocations, including help, version, about, and no arguments, check only an already-installed skill in the standard location. Skill-management commands skip this check. Compare versions with PEP 440. Replace pristine older managed content. Preserve equal or newer versions and unmanaged content. Preserve older content with a missing, malformed, or mismatched hash and recommend `uvx imap-agent-cli skill install --force`.
331
+
332
+ The legacy HTML marker remains recognized unless front matter assigns ownership to another tool. Legacy content without a version migrates as version 0. Missing or malformed managed versions intentionally recover through replacement without integrity verification. Newly generated metadata becomes authoritative.
333
+
334
+ `skill install` creates missing skills and updates pristine older managed skills. `--force` permits replacement of altered managed content, including at the same version. It never overwrites unmanaged content or downgrades a newer version. `skill remove` manages only `SKILL.md`, keeps unrelated files, and retains its explicit force override for unmanaged files. Unexpected directories and linked skill paths are refused.
335
+
336
+ `skill status` is read-only. JSON is the default, with `--format plain` available. Report path, installation and ownership, running and installed versions, version relationship, integrity, automatic eligibility, local-development exclusion, and any force recommendation. All skill commands accept `--skills-dir PATH`. Custom locations require explicit updates.
337
+
338
+ Automatic synchronization excludes local checkouts, local source installations, editable builds, and unverified installation origins. Use installed-distribution file records and PEP 610 metadata. Built wheel installations remain eligible. Explicit skill commands work from development builds.
339
+
340
+ Synchronization performs no package-index lookup, uv cache refresh, or CLI update. Replacement uses a flushed and closed temporary file in the target directory and an atomic replace after rechecking the installed bytes. Concurrent changes abort that attempt. Failures do not change the primary command's exit status. Notices go only to stderr. Updates apply to future skill loading and may not change instructions already loaded in an agent session.
341
+
322
342
  ## Folder Commands
323
343
 
324
344
  List folders:
@@ -1,3 +1,3 @@
1
1
  """Agent-first IMAP CLI."""
2
2
 
3
- __version__ = "0.1.6"
3
+ __version__ = "0.2.0"
@@ -12,7 +12,7 @@ from .errors import AppError, ConfigError
12
12
  from .imap_client import ImapSession
13
13
  from .mime import create_draft_message, header_value, parse_message
14
14
  from .render import write_error, write_json
15
- from .skill import install_skill, remove_skill
15
+ from .skill import install_skill, remove_skill, skill_status, sync_skill
16
16
 
17
17
 
18
18
  PROJECT_URL = "https://github.com/pseudosavant/imap-agent-cli"
@@ -52,8 +52,9 @@ Output:
52
52
  Commands:
53
53
  config initialize and manage profile configuration
54
54
  profiles list configured profile names
55
- install-skill install or update the imap agent skill
56
- remove-skill remove the managed imap agent skill
55
+ skill install, inspect status, or remove the imap agent skill
56
+ install-skill alias for skill install
57
+ remove-skill alias for skill remove
57
58
  folders list folders
58
59
  search search messages
59
60
  read read a message by folder and UID
@@ -61,6 +62,14 @@ Commands:
61
62
  attachments list or download attachments
62
63
  draft create new or reply drafts
63
64
 
65
+ Agent skill:
66
+ uvx imap-agent-cli skill install
67
+ uvx imap-agent-cli skill status
68
+ uvx imap-agent-cli skill install --force
69
+ Installed managed skills in ~/.agents/skills/imap synchronize to newer CLI versions.
70
+ Modified skills are preserved. Local development builds skip synchronization.
71
+ Custom locations require explicit updates with --skills-dir PATH.
72
+
64
73
  More help:
65
74
  imap-agent-cli <command> --help
66
75
  imap-agent-cli --about
@@ -271,15 +280,46 @@ def cmd_profiles(args: argparse.Namespace) -> int:
271
280
 
272
281
 
273
282
  def cmd_install_skill(args: argparse.Namespace) -> int:
274
- write_json(install_skill(Path(args.skills_dir) if args.skills_dir else None))
283
+ write_json(_skill_result(args, install_skill, force=args.force))
275
284
  return 0
276
285
 
277
286
 
278
287
  def cmd_remove_skill(args: argparse.Namespace) -> int:
279
- write_json(remove_skill(Path(args.skills_dir) if args.skills_dir else None, force=args.force))
288
+ write_json(_skill_result(args, remove_skill, force=args.force))
289
+ return 0
290
+
291
+
292
+ def _skill_result(args: argparse.Namespace, operation: Any, **kwargs: Any) -> dict[str, Any]:
293
+ try:
294
+ return operation(Path(args.skills_dir).expanduser() if args.skills_dir else None, **kwargs)
295
+ except (OSError, UnicodeError) as exc:
296
+ raise AppError("invalid_request", "skill operation failed. Check the skill path, UTF-8 encoding, and file permissions.") from exc
297
+
298
+
299
+ def cmd_skill_status(args: argparse.Namespace) -> int:
300
+ result = _skill_result(args, skill_status)
301
+ if args.format == "plain":
302
+ for key, value in result.items():
303
+ rendered = json.dumps(value) if value is None or isinstance(value, bool) else str(value)
304
+ sys.stdout.write(f"{key.replace('_', ' ')}: {rendered}\n")
305
+ else:
306
+ write_json(result)
280
307
  return 0
281
308
 
282
309
 
310
+ def _skill_parser(parser: argparse.ArgumentParser, action: str) -> None:
311
+ parser.add_argument("--skills-dir", metavar="PATH", help="skills root directory (default: ~/.agents/skills). Custom locations require explicit updates")
312
+ if action == "install":
313
+ parser.add_argument("--force", action="store_true", help="replace altered managed content. Never overwrite unmanaged skills or downgrade a newer version")
314
+ parser.set_defaults(func=cmd_install_skill)
315
+ elif action == "remove":
316
+ parser.add_argument("--force", action="store_true", help="allow removal of an unmanaged SKILL.md. Keep unrelated files")
317
+ parser.set_defaults(func=cmd_remove_skill)
318
+ else:
319
+ parser.add_argument("--format", choices=["json", "plain"], default="json", help="status output format (default: json). This command never modifies the skill")
320
+ parser.set_defaults(func=cmd_skill_status)
321
+
322
+
283
323
  def cmd_folders(args: argparse.Namespace) -> int:
284
324
  with _session(args) as session:
285
325
  write_json(session.folders())
@@ -501,14 +541,16 @@ def build_parser() -> argparse.ArgumentParser:
501
541
  profiles = sub.add_parser("profiles", help="list configured profile names")
502
542
  profiles.set_defaults(func=cmd_profiles)
503
543
 
504
- install_skill_parser = sub.add_parser("install-skill", help="install or update the imap agent skill")
505
- install_skill_parser.add_argument("--skills-dir")
506
- install_skill_parser.set_defaults(func=cmd_install_skill)
507
-
508
- remove_skill_parser = sub.add_parser("remove-skill", help="remove the managed imap agent skill")
509
- remove_skill_parser.add_argument("--skills-dir")
510
- remove_skill_parser.add_argument("--force", action="store_true")
511
- remove_skill_parser.set_defaults(func=cmd_remove_skill)
544
+ skill = sub.add_parser("skill", help="manage the imap agent skill", description="Manage SKILL.md explicitly. These commands skip automatic synchronization.")
545
+ skill_sub = skill.add_subparsers(dest="skill_command", required=True)
546
+ for action, description in (
547
+ ("install", "install a missing skill or update pristine older managed content"),
548
+ ("remove", "remove SKILL.md and keep unrelated files"),
549
+ ("status", "inspect version, integrity, and automatic synchronization eligibility without writing"),
550
+ ):
551
+ _skill_parser(skill_sub.add_parser(action, help=description, description=description), action)
552
+ _skill_parser(sub.add_parser("install-skill", help="alias for skill install"), "install")
553
+ _skill_parser(sub.add_parser("remove-skill", help="alias for skill remove"), "remove")
512
554
 
513
555
  folders = sub.add_parser("folders", help="list folders and folder metadata")
514
556
  _common_profile_args(folders)
@@ -616,6 +658,11 @@ def build_parser() -> argparse.ArgumentParser:
616
658
  def main(argv: list[str] | None = None) -> int:
617
659
  if argv is None:
618
660
  argv = sys.argv[1:]
661
+ # Global options take no values. Recognize the command before argparse can
662
+ # exit for help or version, including help for skill-management commands.
663
+ command = next((arg for arg in argv if not arg.startswith("-")), None)
664
+ if command not in {"skill", "install-skill", "remove-skill"}:
665
+ sync_skill()
619
666
  if not argv or argv in (["--help"], ["-h"]):
620
667
  sys.stdout.write(TOP_LEVEL_HELP)
621
668
  return 0
@@ -0,0 +1,41 @@
1
+ """Identify installed code without depending on its launcher."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from importlib import metadata
7
+ from pathlib import Path
8
+ from urllib.parse import urlsplit
9
+
10
+
11
+ DISTRIBUTION_NAME = "imap-agent-cli"
12
+
13
+
14
+ def is_local_development() -> bool:
15
+ """Skip local sources and code whose installed origin cannot be verified.
16
+
17
+ A local wheel is a built distribution, unlike a local source archive or
18
+ directory. Its PEP 610 archive metadata therefore remains eligible.
19
+ """
20
+ try:
21
+ distribution = metadata.distribution(DISTRIBUTION_NAME)
22
+ module = Path(__file__).resolve()
23
+ files = distribution.files or ()
24
+ if not any(
25
+ str(item).replace("\\", "/") == "imap_agent_cli/runtime.py"
26
+ and Path(distribution.locate_file(item)).resolve() == module
27
+ for item in files
28
+ ):
29
+ return True
30
+ direct_url = distribution.read_text("direct_url.json")
31
+ if direct_url is None:
32
+ return False
33
+ source = json.loads(direct_url)
34
+ url = urlsplit(source["url"])
35
+ if "dir_info" in source:
36
+ return True
37
+ if url.scheme == "file":
38
+ return not (url.path.lower().endswith(".whl") and isinstance(source.get("archive_info"), dict))
39
+ return not bool(url.scheme)
40
+ except (metadata.PackageNotFoundError, OSError, ValueError, KeyError, TypeError, AttributeError):
41
+ return True