aisc-cli 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 (99) hide show
  1. {aisc_cli-0.1.1/src/aisc_cli.egg-info → aisc_cli-0.1.2}/PKG-INFO +12 -11
  2. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/README.md +11 -10
  3. aisc_cli-0.1.2/src/aisc/VERSION +1 -0
  4. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/cc_switch_provider.py +20 -1
  5. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/docker_lifecycle.py +369 -0
  6. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/update.py +18 -2
  7. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/cc_switch.py +12 -0
  8. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/run.py +39 -37
  9. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/main.py +17 -6
  10. {aisc_cli-0.1.1 → aisc_cli-0.1.2/src/aisc_cli.egg-info}/PKG-INFO +12 -11
  11. aisc_cli-0.1.1/src/aisc/VERSION +0 -1
  12. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/LICENSE +0 -0
  13. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/MANIFEST.in +0 -0
  14. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/pyproject.toml +0 -0
  15. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/setup.cfg +0 -0
  16. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/__init__.py +0 -0
  17. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/__main__.py +0 -0
  18. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/__init__.py +0 -0
  19. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/config_reader.py +0 -0
  20. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/container_registry.py +0 -0
  21. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/data_root_store.py +0 -0
  22. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/docker_.py +0 -0
  23. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/docker_gateway.py +0 -0
  24. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/docker_sdk_backed.py +0 -0
  25. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/maintenance_lock.py +0 -0
  26. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/state_file.py +0 -0
  27. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/system.py +0 -0
  28. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/windows_config_reader.py +0 -0
  29. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/workspace_lease_store.py +0 -0
  30. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/__init__.py +0 -0
  31. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/artifact.py +0 -0
  32. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/bundle_fetch.py +0 -0
  33. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/cc_switch_resolver.py +0 -0
  34. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/config_service.py +0 -0
  35. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/conversation.py +0 -0
  36. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/data_migration.py +0 -0
  37. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/data_root.py +0 -0
  38. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/doctor.py +0 -0
  39. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/legacy_scan.py +0 -0
  40. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/network_subscription.py +0 -0
  41. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/profile_service.py +0 -0
  42. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/provider.py +0 -0
  43. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/repair.py +0 -0
  44. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/resources.py +0 -0
  45. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/runtime.py +0 -0
  46. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/session.py +0 -0
  47. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/tool_versions.py +0 -0
  48. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/toolchain.py +0 -0
  49. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/usage.py +0 -0
  50. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/version.py +0 -0
  51. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/web_gateway.py +0 -0
  52. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/workspace_reconcile.py +0 -0
  53. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/applog.py +0 -0
  54. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/__init__.py +0 -0
  55. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/build_progress_parser.py +0 -0
  56. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/__init__.py +0 -0
  57. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/agents.py +0 -0
  58. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/artifact.py +0 -0
  59. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/build.py +0 -0
  60. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/bundle.py +0 -0
  61. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/config.py +0 -0
  62. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/container.py +0 -0
  63. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/conversation.py +0 -0
  64. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/data_root.py +0 -0
  65. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/logs.py +0 -0
  66. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/network.py +0 -0
  67. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/profile.py +0 -0
  68. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/provider.py +0 -0
  69. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/runs.py +0 -0
  70. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/runtime.py +0 -0
  71. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/serve.py +0 -0
  72. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/serve_fs.py +0 -0
  73. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/session.py +0 -0
  74. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/update.py +0 -0
  75. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/usage.py +0 -0
  76. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/wizard.py +0 -0
  77. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/workspaces.py +0 -0
  78. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/help_i18n.py +0 -0
  79. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/output.py +0 -0
  80. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/tutorial.py +0 -0
  81. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/__init__.py +0 -0
  82. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/artifacts.py +0 -0
  83. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/cc_switch_release.py +0 -0
  84. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/config.py +0 -0
  85. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/data_migration.py +0 -0
  86. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/data_root.py +0 -0
  87. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/docker_ownership.py +0 -0
  88. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/gateway.py +0 -0
  89. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/models.py +0 -0
  90. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/toolchain.py +0 -0
  91. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/web_services.py +0 -0
  92. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/workspace_lease.py +0 -0
  93. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/schemas/__init__.py +0 -0
  94. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/schemas/config_schema.py +0 -0
  95. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc_cli.egg-info/SOURCES.txt +0 -0
  96. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc_cli.egg-info/dependency_links.txt +0 -0
  97. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc_cli.egg-info/entry_points.txt +0 -0
  98. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc_cli.egg-info/requires.txt +0 -0
  99. {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aisc-cli
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: AISC CLI — Super Claude workstation management
5
5
  Author: AISC contributors
6
6
  License-Expression: MIT
@@ -122,9 +122,10 @@ AISC 尚未代码签名。SmartScreen 提示时,请先确认文件来自项目
122
122
  | **火山引擎 Ark** | 兼容 OpenAI 格式;需配置接入点 ID |
123
123
  | **智谱 GLM** | GLM-5.2 旗舰模型 |
124
124
  | **Kimi** | 月之暗面 Kimi K3 |
125
- | **Codesome-Group** | 二合一中转;Codex 默认模型 `gpt-5.6-sol`,Claude 侧 `https://v5.codesome.cn/api` |
125
+ | **Codesome V3** | 月卡/按量中转(`sk-` Key,后台建 Key 选分组) |
126
+ | **Codesome 二合一** | V5 月卡中转(`cr-` 卡密即 Key,Claude/GPT 双端通用) |
126
127
 
127
- 所有预置供应商的 Codex 上游格式统一为 **OpenAI Responses API(原生直连)**,存量容器在下次启动时自动刷新(用户自定义值不受影响)。
128
+ 2.1.12 起(D-6.7)镜像**不再预置**任何 provider 行——以上为「添加 provider」的模板清单(Codesome 置顶默认)。所有模板的 Codex 上游走 S9a 形态(本地路由把 Responses 翻译为 Anthropic Messages,直连模板声明的 Anthropic 端点);Codesome 两模板的 Claude 侧不含模型键(官方要求,模型由 Key 的服务端分组路由),并预填官方推荐的自动压缩水位(claude 150k / codex 900k)。
128
129
 
129
130
  ## CLI 使用指南
130
131
 
@@ -390,18 +391,18 @@ MIT License,详见 [LICENSE](https://github.com/wangyuncepu/AISC/blob/main/LIC
390
391
 
391
392
  ### Codesome|Codex 与 Claude Code 二合一服务
392
393
 
393
- Codesome 提供 API 调用形式的二合一月卡:一张 `cr-...` API Key 可分别接入 Claude Code 和 Codex。这是 API 服务,不是 Claude 或 Codex 成品账号。
394
+ Codesome 提供 API 调用形式的接入服务,有两条产品线:**V3**(Claude/GPT 月卡 + 按量,`sk-...` Key,在后台建 Key 并选分组)与**二合一/V5**(月卡,订单里的 `cr-...` 卡密直接就是 Key,Claude/GPT 双端通用,无分组)。这是 API 服务,不是 Claude 或 Codex 成品账号。
394
395
 
395
- **v2.1.7 起 AISC 镜像已内置 Codesome-Group 预置供应商**——Workbench 的 Providers 页或容器内 cc-switch 选择 Codesome-Group、填入 API Key 即可使用,无需手工配置端点。
396
+ **2.1.12 起内置两个 Codesome 模板**(`codesome-v3` / `codesome-2in1`)——Workbench Providers 页「添加 provider」默认选中 Codesome,选对产品线、填入 Key 即可(表单下方「获取服务」直达注册/购买页)。两条线的地址不可混用,V3 的 `sk-` Key 不能配二合一,反之亦然。
396
397
 
397
- 手动配置时的端点(二合一 Key 两端通用,URL 不同不可混用):
398
+ 手动配置时的端点(两线各自两端通用,URL 不同不可混用):
398
399
 
399
- | 使用端 | API URL |
400
- | --- | --- |
401
- | Claude Code / Claude | `https://v5.codesome.cn/api` |
402
- | Codex / OpenAI 格式客户端 | `https://v5.codesome.cn/openai` |
400
+ | 产品线 | Claude 端 | Codex / OpenAI 格式端 |
401
+ | --- | --- | --- |
402
+ | V3(sk-) | `https://cc.codesome.ai` | `https://cc.codesome.ai/v1` |
403
+ | 二合一(cr-) | `https://v5.codesome.cn/api` | `https://v5.codesome.cn/openai` |
403
404
 
404
- Claude Code 通过 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN` 配置;Codex 使用 OpenAI 格式配置。完整教程见 [Codesome 文档](https://doc.codesome.ai/)。
405
+ Claude Code 通过 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN` 配置(加 `CLAUDE_CODE_ATTRIBUTION_HEADER=0`,官方核心三行之一);Codex 使用 OpenAI 格式配置(模型统一 `gpt-5.6-terra`)。可选:自动压缩(claude `CLAUDE_AUTO_COMPACT_WINDOW`,codex `model_auto_compact_token_limit`)到值自动压缩历史。完整教程见 [Codesome 文档](https://doc.codesome.ai/)。
405
406
 
406
407
  购买与开通(注册/下单入口,非服务使用地址):
407
408
 
@@ -91,9 +91,10 @@ AISC 尚未代码签名。SmartScreen 提示时,请先确认文件来自项目
91
91
  | **火山引擎 Ark** | 兼容 OpenAI 格式;需配置接入点 ID |
92
92
  | **智谱 GLM** | GLM-5.2 旗舰模型 |
93
93
  | **Kimi** | 月之暗面 Kimi K3 |
94
- | **Codesome-Group** | 二合一中转;Codex 默认模型 `gpt-5.6-sol`,Claude 侧 `https://v5.codesome.cn/api` |
94
+ | **Codesome V3** | 月卡/按量中转(`sk-` Key,后台建 Key 选分组) |
95
+ | **Codesome 二合一** | V5 月卡中转(`cr-` 卡密即 Key,Claude/GPT 双端通用) |
95
96
 
96
- 所有预置供应商的 Codex 上游格式统一为 **OpenAI Responses API(原生直连)**,存量容器在下次启动时自动刷新(用户自定义值不受影响)。
97
+ 2.1.12 起(D-6.7)镜像**不再预置**任何 provider 行——以上为「添加 provider」的模板清单(Codesome 置顶默认)。所有模板的 Codex 上游走 S9a 形态(本地路由把 Responses 翻译为 Anthropic Messages,直连模板声明的 Anthropic 端点);Codesome 两模板的 Claude 侧不含模型键(官方要求,模型由 Key 的服务端分组路由),并预填官方推荐的自动压缩水位(claude 150k / codex 900k)。
97
98
 
98
99
  ## CLI 使用指南
99
100
 
@@ -359,18 +360,18 @@ MIT License,详见 [LICENSE](https://github.com/wangyuncepu/AISC/blob/main/LIC
359
360
 
360
361
  ### Codesome|Codex 与 Claude Code 二合一服务
361
362
 
362
- Codesome 提供 API 调用形式的二合一月卡:一张 `cr-...` API Key 可分别接入 Claude Code 和 Codex。这是 API 服务,不是 Claude 或 Codex 成品账号。
363
+ Codesome 提供 API 调用形式的接入服务,有两条产品线:**V3**(Claude/GPT 月卡 + 按量,`sk-...` Key,在后台建 Key 并选分组)与**二合一/V5**(月卡,订单里的 `cr-...` 卡密直接就是 Key,Claude/GPT 双端通用,无分组)。这是 API 服务,不是 Claude 或 Codex 成品账号。
363
364
 
364
- **v2.1.7 起 AISC 镜像已内置 Codesome-Group 预置供应商**——Workbench 的 Providers 页或容器内 cc-switch 选择 Codesome-Group、填入 API Key 即可使用,无需手工配置端点。
365
+ **2.1.12 起内置两个 Codesome 模板**(`codesome-v3` / `codesome-2in1`)——Workbench Providers 页「添加 provider」默认选中 Codesome,选对产品线、填入 Key 即可(表单下方「获取服务」直达注册/购买页)。两条线的地址不可混用,V3 的 `sk-` Key 不能配二合一,反之亦然。
365
366
 
366
- 手动配置时的端点(二合一 Key 两端通用,URL 不同不可混用):
367
+ 手动配置时的端点(两线各自两端通用,URL 不同不可混用):
367
368
 
368
- | 使用端 | API URL |
369
- | --- | --- |
370
- | Claude Code / Claude | `https://v5.codesome.cn/api` |
371
- | Codex / OpenAI 格式客户端 | `https://v5.codesome.cn/openai` |
369
+ | 产品线 | Claude 端 | Codex / OpenAI 格式端 |
370
+ | --- | --- | --- |
371
+ | V3(sk-) | `https://cc.codesome.ai` | `https://cc.codesome.ai/v1` |
372
+ | 二合一(cr-) | `https://v5.codesome.cn/api` | `https://v5.codesome.cn/openai` |
372
373
 
373
- Claude Code 通过 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN` 配置;Codex 使用 OpenAI 格式配置。完整教程见 [Codesome 文档](https://doc.codesome.ai/)。
374
+ Claude Code 通过 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN` 配置(加 `CLAUDE_CODE_ATTRIBUTION_HEADER=0`,官方核心三行之一);Codex 使用 OpenAI 格式配置(模型统一 `gpt-5.6-terra`)。可选:自动压缩(claude `CLAUDE_AUTO_COMPACT_WINDOW`,codex `model_auto_compact_token_limit`)到值自动压缩历史。完整教程见 [Codesome 文档](https://doc.codesome.ai/)。
374
375
 
375
376
  购买与开通(注册/下单入口,非服务使用地址):
376
377
 
@@ -0,0 +1 @@
1
+ 0.1.2
@@ -21,7 +21,7 @@ from aisc.domain.models import CliError, RuntimeExitCode
21
21
  PROTOCOL = "aisc.cc-switch-provider/v1"
22
22
  _ADAPTER_PATH = "/usr/local/bin/aisc-cc-provider"
23
23
  _AGENTS = ("claude", "codex")
24
- _OPS = ("list", "add", "edit", "switch", "delete", "fetch-models")
24
+ _OPS = ("list", "add", "edit", "switch", "delete", "fetch-models", "templates")
25
25
 
26
26
 
27
27
  def _validate(runtime_id: str, agent: str, op: str) -> None:
@@ -161,6 +161,25 @@ def list_providers(runtime_id: str, agent: str, workspace: Optional[str], execut
161
161
  "operation_id": envelope.get("operation_id")}
162
162
 
163
163
 
164
+ def list_templates(runtime_id: str, agent: str, workspace: Optional[str],
165
+ executor: Any) -> Dict[str, Any]:
166
+ """D-6.8: the add-provider template manifest (adapter `templates` op).
167
+
168
+ The op itself is agent-less; the transport keeps the uniform --agent
169
+ argument (validated, then ignored inside the adapter)."""
170
+ from aisc.application.data_root import workspace_state_dir
171
+ from pathlib import Path
172
+
173
+ ws_path = Path(workspace).resolve() if workspace else Path.cwd()
174
+ _validate(runtime_id, agent, "templates")
175
+ envelope = _exec_adapter(
176
+ runtime_id, workspace_state_dir(ws_path), executor, "templates", agent,
177
+ None, None,
178
+ )
179
+ return {"agent": agent, "templates": envelope.get("templates") or [],
180
+ "operation_id": envelope.get("operation_id")}
181
+
182
+
164
183
  def add_provider(runtime_id: str, agent: str, request: Dict[str, Any],
165
184
  workspace: Optional[str], executor: Any) -> Dict[str, Any]:
166
185
  from aisc.application.data_root import workspace_state_dir
@@ -31,6 +31,7 @@ Stage C wires the installers' rebuild flow.
31
31
  from __future__ import annotations
32
32
 
33
33
  import json
34
+ import re
34
35
  from pathlib import Path
35
36
  from typing import Any, Dict, Iterable, List, Optional, Tuple
36
37
 
@@ -541,6 +542,374 @@ def _log_tail(stdout: str, lines: int = 20) -> str:
541
542
 
542
543
  CACHE_USAGE_SCHEMA = "aisc.docker-cache-usage/v1"
543
544
  CACHE_CLEANUP_SCHEMA = "aisc.docker-cache-cleanup/v1"
545
+ CACHE_INSPECT_SCHEMA = "aisc.docker-cache-inspect/v1"
546
+
547
+
548
+ # --- v2.1.13 (docker-scan-fidelity): read-only per-resource inspection -----
549
+ #
550
+ # D-2 (2026-09-19): detect FINER and report ACCURATELY; never clean anything
551
+ # automatically, never add new prune paths. Primary size/detail source is
552
+ # `docker system df -v --format json` (Docker 29.x returns structured
553
+ # per-item JSON there — verified on 29.8.0; officially undocumented, so
554
+ # callers must tolerate absence). Fallbacks degrade per-category and mark
555
+ # rows unknown; nothing here blocks anything else.
556
+ #
557
+ # Accuracy notes (the "更准确" half):
558
+ # - Image RECLAIMABLE aggregates over-count shared layers; the accurate
559
+ # per-image figure is UNIQUE SIZE, summed over images with Containers==0.
560
+ # - `image prune` (no -a) only ever removes DANGLING images, so rows also
561
+ # carry will_be_cleaned to show what the CURRENT manual cleanup would hit.
562
+ # - Build cache has no Reclaimable boolean in df -v; it is derived from
563
+ # InUse. buildx du (fallback) carries it natively.
564
+
565
+ _SIZE_UNITS: Dict[str, int] = {
566
+ "": 1, "B": 1,
567
+ "K": 10**3, "KI": 2**10,
568
+ "M": 10**6, "MI": 2**20,
569
+ "G": 10**9, "GI": 2**30,
570
+ "T": 10**12, "TI": 2**40,
571
+ }
572
+
573
+
574
+ def _size_bytes(value: Any) -> Optional[int]:
575
+ """Best-effort docker size strings (`35.6 kB`, `1.2GB`, ints) → bytes.
576
+ None for N/A / unparseable — the row is reported as unknown."""
577
+ if value is None:
578
+ return None
579
+ if isinstance(value, (int, float)):
580
+ return int(value)
581
+ s = str(value).strip()
582
+ if not s or s.upper() in ("N/A", "UNKNOWN"):
583
+ return None
584
+ m = re.match(r"^([\d.]+)\s*([A-Za-z]*)$", s)
585
+ if not m:
586
+ return None
587
+ try:
588
+ num = float(m.group(1))
589
+ except ValueError:
590
+ return None
591
+ unit = m.group(2).upper().rstrip("B") if m.group(2) else ""
592
+ if unit not in _SIZE_UNITS:
593
+ return None
594
+ return int(num * _SIZE_UNITS[unit])
595
+
596
+
597
+ def _container_rw_size(value: Any) -> Optional[int]:
598
+ """`ps -s` size strings: `35.6 kB (virtual 109 MB)` → writable layer."""
599
+ if value is None:
600
+ return None
601
+ head = str(value).split("(virtual")[0].strip()
602
+ return _size_bytes(head)
603
+
604
+
605
+ def _json_lines(stdout: str) -> List[Dict[str, Any]]:
606
+ out: List[Dict[str, Any]] = []
607
+ for line in (stdout or "").splitlines():
608
+ line = line.strip()
609
+ if not line.startswith("{"):
610
+ continue
611
+ try:
612
+ row = json.loads(line)
613
+ except ValueError:
614
+ continue
615
+ if isinstance(row, dict):
616
+ out.append(row)
617
+ return out
618
+
619
+
620
+ def _df_verbose(executor: Any) -> Optional[Dict[str, Any]]:
621
+ """`docker system df -v --format json` → {Images, Containers, Volumes,
622
+ BuildCache} (no Networks section). None when unavailable."""
623
+ try:
624
+ result = executor.run_captured(
625
+ ["system", "df", "-v", "--format", "json"], timeout=60.0
626
+ )
627
+ except Exception:
628
+ return None
629
+ if getattr(result, "exit_code", 1) != 0:
630
+ return None
631
+ text = (result.stdout or "").strip()
632
+ if not text:
633
+ return None
634
+ try:
635
+ doc = json.loads(text)
636
+ if isinstance(doc, dict):
637
+ return doc
638
+ except ValueError:
639
+ pass
640
+ merged: Dict[str, Any] = {}
641
+ for row in _json_lines(text):
642
+ merged.update(row)
643
+ return merged or None
644
+
645
+
646
+ def _buildx_du(executor: Any) -> Optional[List[Dict[str, Any]]]:
647
+ """`docker buildx du --format json` (NDJSON records). None when the
648
+ buildx plugin is absent/old (Docker Desktop's `docker builder` aliases
649
+ to buildx; classic CLIs lack du entirely)."""
650
+ try:
651
+ result = executor.run_captured(
652
+ ["buildx", "du", "--format", "json"], timeout=60.0
653
+ )
654
+ except Exception:
655
+ return None
656
+ if getattr(result, "exit_code", 1) != 0:
657
+ return None
658
+ rows = _json_lines(result.stdout or "")
659
+ return rows or None
660
+
661
+
662
+ def _images_rows(executor: Any, df_v: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:
663
+ """Image rows. df -v Images is the ONLY real Unique/Shared source on
664
+ Docker 29 (image ls --format json reports N/A placeholders there); the
665
+ ls pass adds dangling classification for older daemons. Reclaimable =
666
+ Σ UniqueSize over images with no containers; dangling is the subset the
667
+ current manual cleanup would remove (its 24h age filter is NOT applied
668
+ per-row — the disclaimer says so)."""
669
+ rows: List[Dict[str, Any]] = []
670
+ ls = executor.run_captured(["images", "--format", "{{json .}}"], timeout=30.0)
671
+ fallback: List[Dict[str, Any]] = []
672
+ if getattr(ls, "exit_code", 1) == 0:
673
+ for row in _json_lines(ls.stdout or ""):
674
+ repo = str(row.get("Repository") or "")
675
+ fallback.append({
676
+ "id": str(row.get("ID") or ""),
677
+ "name": f"{repo}:{row.get('Tag')}",
678
+ "in_use": None,
679
+ "dangling": repo == "<none>",
680
+ "size": row.get("Size"),
681
+ "size_bytes": _size_bytes(row.get("Size")),
682
+ "unique_size": None,
683
+ "shared": None,
684
+ "reclaimable_bytes": None,
685
+ "will_be_cleaned": repo == "<none>",
686
+ })
687
+ dv = (df_v or {}).get("Images")
688
+ if not isinstance(dv, list) or not dv:
689
+ return fallback
690
+ for row in dv:
691
+ if not isinstance(row, dict):
692
+ continue
693
+ containers = str(row.get("Containers") or "").strip()
694
+ in_use = containers not in ("", "0", "N/A")
695
+ repo = str(row.get("Repository") or "")
696
+ dangling = repo == "<none>"
697
+ unique = _size_bytes(row.get("UniqueSize"))
698
+ rows.append({
699
+ "id": str(row.get("ID") or ""),
700
+ "name": f"{repo}:{row.get('Tag')}",
701
+ "in_use": in_use,
702
+ "dangling": dangling,
703
+ "size": row.get("Size"),
704
+ "size_bytes": _size_bytes(row.get("Size")),
705
+ "unique_size": row.get("UniqueSize"),
706
+ # SharedSize is a display STRING (e.g. "326.5MB") - it must never
707
+ # ride the boolean shared field (2.1.13 hand-test: the string
708
+ # made the Rust serde drop the whole images category).
709
+ "shared": None,
710
+ "reclaimable_bytes": (0 if in_use else unique),
711
+ "will_be_cleaned": dangling,
712
+ })
713
+ return rows
714
+
715
+
716
+ def _container_rows(executor: Any) -> List[Dict[str, Any]]:
717
+ """Stopped containers' writable layers are the reclaimable part; running
718
+ ones report unknown (they cannot be reclaimed without stopping)."""
719
+ ps = executor.run_captured(
720
+ ["ps", "-a", "--size", "--format", "{{json .}}"], timeout=30.0
721
+ )
722
+ rows: List[Dict[str, Any]] = []
723
+ if getattr(ps, "exit_code", 1) != 0:
724
+ return rows
725
+ for row in _json_lines(ps.stdout or ""):
726
+ names = row.get("Names") or row.get("Name") or ""
727
+ name = ", ".join(str(n) for n in names) if isinstance(names, list) else str(names)
728
+ state = str(row.get("State") or "")
729
+ running = state == "running"
730
+ rw = _container_rw_size(row.get("Size"))
731
+ rows.append({
732
+ "id": str(row.get("ID") or ""),
733
+ "name": name,
734
+ "image": str(row.get("Image") or ""),
735
+ "state": state,
736
+ "size": row.get("Size"),
737
+ "size_bytes": None if running else _container_rw_size(row.get("Size")),
738
+ "in_use": running,
739
+ "reclaimable_bytes": (None if running else rw),
740
+ "will_be_cleaned": False, # AISC never removes user containers here
741
+ })
742
+ return rows
743
+
744
+
745
+ def _volume_rows(executor: Any) -> List[Dict[str, Any]]:
746
+ """Named/anonymous × in-use/dangling. Sizes stay unknown by default
747
+ (per-volume du can take minutes); AISC NEVER deletes volumes (02 §5) —
748
+ every row is will_be_cleaned=False and the UI says so."""
749
+ ls = executor.run_captured(["volume", "ls", "--format", "{{json .}}"], timeout=30.0)
750
+ if getattr(ls, "exit_code", 1) != 0:
751
+ return []
752
+ dang = executor.run_captured(
753
+ ["volume", "ls", "--filter", "dangling=true", "--format", "{{json .}}"],
754
+ timeout=30.0,
755
+ )
756
+ dangling: set = set()
757
+ if getattr(dang, "exit_code", 1) == 0:
758
+ for row in _json_lines(dang.stdout or ""):
759
+ name = str(row.get("Name") or "")
760
+ if name:
761
+ dangling.add(name)
762
+ rows: List[Dict[str, Any]] = []
763
+ for row in _json_lines(ls.stdout or ""):
764
+ name = str(row.get("Name") or "")
765
+ if not name:
766
+ continue
767
+ rows.append({
768
+ "id": name,
769
+ "name": name,
770
+ "dangling": name in dangling,
771
+ "in_use": name not in dangling,
772
+ "size": row.get("Size"),
773
+ "size_bytes": _size_bytes(row.get("Size")),
774
+ "reclaimable_bytes": None, # volumes are never reclaimed by AISC
775
+ "will_be_cleaned": False,
776
+ })
777
+ return rows
778
+
779
+
780
+ def _cache_rows(executor: Any, df_v: Optional[Dict[str, Any]]) -> Tuple[List[Dict[str, Any]], bool]:
781
+ """Build-cache rows + whether the buildx fallback ran. Primary: df -v
782
+ BuildCache (daemon-wide, deduped; Reclaimable derived from InUse).
783
+ Fallback: buildx du (Reclaimable natively; single builder)."""
784
+ rows: List[Dict[str, Any]] = []
785
+ dv = (df_v or {}).get("BuildCache")
786
+ if isinstance(dv, list) and dv:
787
+ for row in dv:
788
+ if not isinstance(row, dict):
789
+ continue
790
+ in_use = bool(row.get("InUse"))
791
+ size_bytes = _size_bytes(row.get("Size"))
792
+ rows.append({
793
+ "id": str(row.get("ID") or ""),
794
+ "kind": str(row.get("CacheType") or row.get("Type") or ""),
795
+ "in_use": in_use,
796
+ "shared": bool(row.get("Shared")),
797
+ "size": row.get("Size"),
798
+ "size_bytes": size_bytes,
799
+ "reclaimable_bytes": (0 if in_use else (size_bytes or 0)),
800
+ "will_be_cleaned": not in_use,
801
+ "last_used_at": row.get("LastUsedAt"),
802
+ "usage_count": row.get("UsageCount"),
803
+ })
804
+ return rows, False
805
+ bx = _buildx_du(executor)
806
+ if bx is not None:
807
+ for row in bx:
808
+ reclaimable = bool(row.get("Reclaimable"))
809
+ size_bytes = _size_bytes(row.get("Size"))
810
+ rows.append({
811
+ "id": str(row.get("ID") or ""),
812
+ "kind": str(row.get("Type") or row.get("CacheType") or ""),
813
+ "in_use": not reclaimable,
814
+ "shared": bool(row.get("Shared")),
815
+ "size": row.get("Size"),
816
+ "size_bytes": size_bytes,
817
+ "reclaimable_bytes": ((size_bytes or 0) if reclaimable else 0),
818
+ "will_be_cleaned": reclaimable,
819
+ "last_used_at": row.get("LastUsedAt"),
820
+ "usage_count": row.get("UsageCount"),
821
+ })
822
+ return rows, True
823
+ return rows, False
824
+
825
+
826
+ def _network_rows(executor: Any) -> List[Dict[str, Any]]:
827
+ """Networks are 0-byte; only the count/reference picture matters."""
828
+ ls = executor.run_captured(["network", "ls", "--format", "{{json .}}"], timeout=30.0)
829
+ rows: List[Dict[str, Any]] = []
830
+ if getattr(ls, "exit_code", 1) != 0:
831
+ return rows
832
+ for row in _json_lines(ls.stdout or ""):
833
+ name = str(row.get("Name") or "")
834
+ if not name:
835
+ continue
836
+ rows.append({
837
+ "id": name,
838
+ "name": name,
839
+ "kind": str(row.get("Driver") or ""),
840
+ "in_use": None, # reference counting rides the containers pass
841
+ "size_bytes": 0,
842
+ "reclaimable_bytes": 0,
843
+ "will_be_cleaned": False,
844
+ })
845
+ return rows
846
+
847
+
848
+ def _category_summary(rows: List[Dict[str, Any]]) -> Dict[str, Any]:
849
+ reclaimable = 0
850
+ unknown = 0
851
+ for row in rows:
852
+ value = row.get("reclaimable_bytes")
853
+ if value is None:
854
+ unknown += 1
855
+ else:
856
+ reclaimable += int(value)
857
+ return {
858
+ "count": len(rows),
859
+ "reclaimable_bytes": reclaimable,
860
+ "unknown_count": unknown,
861
+ }
862
+
863
+
864
+ def cache_inspect(executor: Any, *, cleanup_min_age_hours: int = 24) -> Dict[str, Any]:
865
+ """Read-only per-resource inspection (D-2). Detects images / containers /
866
+ volumes / build cache / networks item-by-item, reports the deduped
867
+ (UNIQUE-size) reclaimable picture, and flags what the CURRENT manual
868
+ cleanup would hit. Absolutely no prune/rmi/rm anywhere in here, and no
869
+ caller ever chains this into cleanup."""
870
+ df = _system_df(executor)
871
+ docker_available = bool(df)
872
+ warnings: List[str] = []
873
+ df_v: Optional[Dict[str, Any]] = None
874
+ buildx_used = False
875
+ if docker_available:
876
+ df_v = _df_verbose(executor)
877
+ if df_v is None:
878
+ warnings.append("df-verbose-json-unavailable")
879
+ buildx_used = _buildx_du(executor) is None
880
+ images = _images_rows(executor, df_v)
881
+ containers = _container_rows(executor)
882
+ volumes = _volume_rows(executor)
883
+ cache_rows, buildx_used = _cache_rows(executor, df_v)
884
+ networks = _network_rows(executor)
885
+
886
+ def cat(rows: List[Dict[str, Any]]) -> Dict[str, Any]:
887
+ return {"rows": rows, "summary": _category_summary(rows)}
888
+
889
+ return {
890
+ "schema_version": CACHE_INSPECT_SCHEMA,
891
+ "action": "cache-inspect",
892
+ "docker_available": docker_available,
893
+ "categories": {
894
+ "images": cat(images),
895
+ "containers": cat(containers),
896
+ "volumes": cat(volumes),
897
+ "build_cache": cat(cache_rows),
898
+ "networks": cat(networks),
899
+ },
900
+ "capabilities": {
901
+ "df_verbose_json": df_v is not None,
902
+ "buildx_du": buildx_used,
903
+ },
904
+ "cleanup_min_age_hours": cleanup_min_age_hours,
905
+ "warnings": warnings,
906
+ "disclaimer": (
907
+ "Reclaimable figures are estimates (UNIQUE-size based, deduped); "
908
+ "actual freed space can be lower. will_be_cleaned approximates "
909
+ "the current manual cleanup (dangling images / not-in-use build "
910
+ "cache) without its age filter."
911
+ ),
912
+ }
544
913
 
545
914
 
546
915
  def _cache_cleanup_argv(kind: str, min_age_hours: int) -> List[str]:
@@ -182,7 +182,12 @@ def perform_update(
182
182
 
183
183
  ``workdir`` overrides the exe directory (tests). Raises UpdateError /
184
184
  BundleFetchError fail-closed; nothing is replaced unless every check
185
- passed."""
185
+ passed.
186
+
187
+ D-8 (2026-09-19): releases no longer ship CLI archives — the NSIS
188
+ installer IS the CLI+UI update channel for frozen installs, and pip is
189
+ the other. The download path therefore only works with an explicit
190
+ ``--from-file`` archive (manual/offline updates)."""
186
191
  import hashlib
187
192
 
188
193
  if form["form"] == "pip":
@@ -202,8 +207,19 @@ def perform_update(
202
207
  UPDATE_ERROR_STATE,
203
208
  "no final release published yet — nothing to update to "
204
209
  "(explicit --version <ver> to force)")
205
- if not version and _version_key(target) <= _version_key(cli_version):
210
+ if not from_file and not version and _version_key(target) <= _version_key(cli_version):
206
211
  return {"status": "up-to-date", "cli_version": cli_version, "latest_final": target}
212
+ # D-8 (2026-09-19): releases no longer ship CLI archives. The online
213
+ # sidecar-swap is retired — NSIS installs update the CLI with the
214
+ # Workbench, pip installs via pip. Manual/offline updates keep working
215
+ # through --from-file.
216
+ if not from_file:
217
+ raise UpdateError(
218
+ UPDATE_ERROR_USAGE,
219
+ "aisc update no longer downloads CLI archives (releases ship "
220
+ "the Workbench installer only). Update paths: pip install -U "
221
+ "aisc-cli · Workbench 检查更新(自带最新 CLI)· or --from-file "
222
+ "<archive> --sha256 <hex> for a manual archive")
207
223
 
208
224
  exe = Path(form["exe"]) if not workdir else workdir / Path(form["exe"]).name
209
225
  exe_dir = exe.parent
@@ -55,6 +55,18 @@ def cmd_cc_switch_list(args: Any) -> Dict[str, Any]:
55
55
  )
56
56
 
57
57
 
58
+ def cmd_cc_switch_templates(args: Any) -> Dict[str, Any]:
59
+ """D-6.8: the add-provider template manifest (agent-less adapter op)."""
60
+ from aisc.application.cc_switch_provider import list_templates
61
+
62
+ return list_templates(
63
+ runtime_id=args.runtime_id,
64
+ agent=args.agent,
65
+ workspace=args.workspace,
66
+ executor=None,
67
+ )
68
+
69
+
58
70
  def cmd_cc_switch_add(args: Any) -> Dict[str, Any]:
59
71
  from aisc.application.cc_switch_provider import add_provider
60
72
 
@@ -155,12 +155,6 @@ def plan_run(
155
155
  # Run result (structured outcome)
156
156
  # ---------------------------------------------------------------------------
157
157
 
158
- def _name_series(container_name: str) -> str:
159
- """Container names are ``<alias>-<hash8>``; the alias may itself contain
160
- dashes, the hash never does — strip the last segment to get the series
161
- (rename detection keys off it, r2 #C)."""
162
- return container_name.rsplit("-", 1)[0]
163
-
164
158
 
165
159
  @dataclass
166
160
  class RunResult:
@@ -177,9 +171,6 @@ class RunResult:
177
171
  # r1 #2: older same-workspace containers this activation swept
178
172
  # (stopped+removed+unregistered before the new one started).
179
173
  replaced: List[str] = field(default_factory=list)
180
- # r1 #2 (revised): a LIVE same-workspace container was REUSED — nothing
181
- # was started; the summary tells the user it is already open.
182
- reused: Optional[str] = None
183
174
 
184
175
  def to_dict(self) -> Dict[str, Any]:
185
176
  out = {
@@ -194,8 +185,6 @@ class RunResult:
194
185
  out["web_gateway"] = dict(self.web_gateway)
195
186
  if self.replaced:
196
187
  out["replaced"] = list(self.replaced)
197
- if self.reused:
198
- out["reused"] = self.reused
199
188
  return out
200
189
 
201
190
 
@@ -353,21 +342,21 @@ def run_container(
353
342
  )
354
343
  # EXISTS → proceed
355
344
 
356
- # --- r1 #2 (revised) + r2 #C: same-workspace activation is IDEMPOTENT;
357
- # a DIFFERENT --name is a rename intent and rebuilds ---
358
- # A default (label-less) activation owns the workspace's single CLI
359
- # slot. A LIVE older container of the SAME name series is REUSED — the
360
- # user chose to run a workspace that is already open, so tell them
361
- # (and how to reach or rebuild it) instead of churning containers under
362
- # them. A live container of a DIFFERENT series (explicit --name that
363
- # disagrees with the existing one) is a rename: rebuild under the new
364
- # name (alias and container name must stay one story). Only DEAD ones
365
- # (Exited/gone — nobody is using them) are swept (stop+rm+unregister)
366
- # before the new start. GUI runtimes (owner=workbench, lease-guarded)
367
- # and --label multi-container slots are never touched. If docker does
368
- # not answer the liveness probe, judge nothing: fall through to the
369
- # normal start path rather than risk tearing down a live container we
370
- # could not see.
345
+ # --- r1 #2 + r2 #C, revised 2026-09-19 (cli-run-guard): `aisc run` is
346
+ # FORBIDDEN while a live container exists for this workspace. Previously
347
+ # a live same-series container was silently reused, a rename intent
348
+ # rebuilt around live containers, and workbench-owned runtimes were
349
+ # bypassed (stacking a one-shot on top of a managed runtime). Now any
350
+ # LIVE label-less container of this workspace (CLI- or workbench-owned,
351
+ # any name series) refuses the run with guidance. DEAD CLI-owned
352
+ # containers are still swept (stop+rm+unregister) before the new start —
353
+ # sweeping a dead different-series container is the rename path (r2 #C).
354
+ # Dead workbench singletons stay untouched (the Workbench/reconcile owns
355
+ # their lifecycle). --label slots remain a bypass activation: neither
356
+ # checked nor touched. If docker does not answer the liveness probe,
357
+ # judge nothing: fall through to the normal start path rather than risk
358
+ # tearing down a live container we could not see.
359
+ live_blockers: List[tuple] = []
371
360
  if not plan.dry_run and not plan.label:
372
361
  from aisc.adapters.container_registry import list_containers as _lc
373
362
  from aisc.adapters.container_registry import unregister
@@ -386,8 +375,6 @@ def run_container(
386
375
  _states[_nm2] = _st
387
376
  for _nm, _meta in _lc(_reg).items():
388
377
  _meta = _meta if isinstance(_meta, dict) else {}
389
- if _meta.get("owner") == "workbench":
390
- continue
391
378
  if _meta.get("label"):
392
379
  continue
393
380
  try:
@@ -395,13 +382,16 @@ def run_container(
395
382
  continue
396
383
  except OSError:
397
384
  continue
398
- if (_states.get(str(_nm), "").startswith("Up")
399
- and _name_series(str(_nm)) == _name_series(plan.name)):
400
- if result.reused is None:
401
- result.reused = str(_nm)
385
+ if (_states.get(str(_nm), "") or "").startswith("Up"):
386
+ live_blockers.append(
387
+ (str(_nm), str(_meta.get("owner") or "cli")))
388
+ continue
389
+ if _meta.get("owner") == "workbench":
390
+ # dead GUI singleton: the Workbench/reconcile owns its
391
+ # lifecycle — never swept from the CLI path
402
392
  continue
403
- # dead, unknown-to-docker, or a different name series
404
- # (rename intent) → sweep and rebuild
393
+ # dead or unknown-to-docker CLI-owned (any series — a
394
+ # different series is the rename path) → sweep and rebuild
405
395
  exec_.run_captured(["stop", _nm], timeout=30.0)
406
396
  _rm = exec_.run_captured(["rm", "-f", _nm], timeout=30.0)
407
397
  if _rm.exit_code == 0 or "no such" in (_rm.stderr or "").lower():
@@ -413,9 +403,21 @@ def run_container(
413
403
  except Exception:
414
404
  pass
415
405
 
416
- if result.reused is not None:
417
- result.executed = False
418
- return result
406
+ if live_blockers:
407
+ # raise OUTSIDE the sweeping try/except — a CliError must not be
408
+ # swallowed by the best-effort guard above
409
+ _nm, _owner = live_blockers[0]
410
+ _who = "Workbench 管理的运行时" if _owner == "workbench" else "容器"
411
+ raise CliError(
412
+ message=(
413
+ f"该工作区已存在运行中的{_who}「{_nm}」,aisc run 禁止重复创建。\n"
414
+ f" 进入容器: aisc claude / aisc codex / aisc shell\n"
415
+ f" 生命周期: aisc runtime stop(或 Workbench)停止后再运行\n"
416
+ f" 多实例: aisc run --label <槽位名>"
417
+ ),
418
+ exit_code=6, error_code="AISC_ERR_CONTAINER_EXISTS",
419
+ data=result.to_dict(),
420
+ )
419
421
 
420
422
  # --- register container in the multi-container index ---
421
423
  # F2-C: the registry root is the WORKSPACE's state dir (workspaces/<h>/
@@ -347,6 +347,13 @@ def _build_parser() -> _AiscArgumentParser:
347
347
  help="Additionally emit the FULL api_key of this provider "
348
348
  "(edit-time explicit view, 2.1.11 P1)")
349
349
 
350
+ # D-6.8 (2.1.12): the add-provider template manifest (agent-less op;
351
+ # --agent stays required for transport uniformity and is ignored).
352
+ cst = cssub.add_parser("templates",
353
+ help="Emit the add-provider template manifest",
354
+ allow_abbrev=False)
355
+ _cc_switch_common(cst)
356
+
350
357
  csa = cssub.add_parser("add", help="Add a provider (request JSON on stdin)",
351
358
  allow_abbrev=False)
352
359
  _cc_switch_common(csa)
@@ -516,6 +523,10 @@ def _build_parser() -> _AiscArgumentParser:
516
523
  mtu = mtsub.add_parser("cache-usage", help="Read-only docker system df summary",
517
524
  allow_abbrev=False)
518
525
  _add_global_args(mtu, is_subparser=True)
526
+ mtci = mtsub.add_parser("cache-inspect",
527
+ help="Read-only per-item Docker resource inspection (D-2)",
528
+ allow_abbrev=False)
529
+ _add_global_args(mtci, is_subparser=True)
519
530
 
520
531
  mtcc = mtsub.add_parser("cache-cleanup",
521
532
  help="Prune builder cache + dangling images (until-filtered)",
@@ -1423,12 +1434,6 @@ def _cmd_run(
1423
1434
  ]
1424
1435
 
1425
1436
  if effective_format == "text" and emitter is None:
1426
- if out.get("reused"):
1427
- print(f"ℹ 工作区已在运行中: {plan.workspace}")
1428
- print(f" 容器 {out['reused']}(Up) · 复用现有容器,未新建")
1429
- print(" 进入方式: aisc claude | aisc codex | aisc switch | aisc shell")
1430
- print(" 如需重建: 先 aisc stop 再重新 aisc run")
1431
- return out, 0, []
1432
1437
  print(f"💯 工作区已激活: {plan.workspace}")
1433
1438
  print(f" 容器 {plan.name}(detached) · 镜像 {plan.image} · 网络 {plan.network}")
1434
1439
  if out.get("replaced"):
@@ -1726,6 +1731,8 @@ def _cmd_cc_switch(
1726
1731
  sub = getattr(args, "cc_switch_command", None)
1727
1732
  if sub == "list":
1728
1733
  data = cs_cmd.cmd_cc_switch_list(args)
1734
+ elif sub == "templates":
1735
+ data = cs_cmd.cmd_cc_switch_templates(args)
1729
1736
  elif sub == "add":
1730
1737
  data = cs_cmd.cmd_cc_switch_add(args)
1731
1738
  elif sub == "edit":
@@ -1954,6 +1961,10 @@ def _cmd_maintenance(
1954
1961
  if sub == "cache-usage":
1955
1962
  from aisc.application.docker_lifecycle import cache_usage
1956
1963
  return cache_usage(executor), 0, []
1964
+ if sub == "cache-inspect":
1965
+ # D-2 read-only per-item inspection — never chained into cleanup
1966
+ from aisc.application.docker_lifecycle import cache_inspect
1967
+ return cache_inspect(executor), 0, []
1957
1968
  if sub == "cache-cleanup":
1958
1969
  from aisc.application.docker_lifecycle import docker_cache_cleanup
1959
1970
  data = docker_cache_cleanup(executor, min_age_hours=args.min_age_hours)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aisc-cli
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: AISC CLI — Super Claude workstation management
5
5
  Author: AISC contributors
6
6
  License-Expression: MIT
@@ -122,9 +122,10 @@ AISC 尚未代码签名。SmartScreen 提示时,请先确认文件来自项目
122
122
  | **火山引擎 Ark** | 兼容 OpenAI 格式;需配置接入点 ID |
123
123
  | **智谱 GLM** | GLM-5.2 旗舰模型 |
124
124
  | **Kimi** | 月之暗面 Kimi K3 |
125
- | **Codesome-Group** | 二合一中转;Codex 默认模型 `gpt-5.6-sol`,Claude 侧 `https://v5.codesome.cn/api` |
125
+ | **Codesome V3** | 月卡/按量中转(`sk-` Key,后台建 Key 选分组) |
126
+ | **Codesome 二合一** | V5 月卡中转(`cr-` 卡密即 Key,Claude/GPT 双端通用) |
126
127
 
127
- 所有预置供应商的 Codex 上游格式统一为 **OpenAI Responses API(原生直连)**,存量容器在下次启动时自动刷新(用户自定义值不受影响)。
128
+ 2.1.12 起(D-6.7)镜像**不再预置**任何 provider 行——以上为「添加 provider」的模板清单(Codesome 置顶默认)。所有模板的 Codex 上游走 S9a 形态(本地路由把 Responses 翻译为 Anthropic Messages,直连模板声明的 Anthropic 端点);Codesome 两模板的 Claude 侧不含模型键(官方要求,模型由 Key 的服务端分组路由),并预填官方推荐的自动压缩水位(claude 150k / codex 900k)。
128
129
 
129
130
  ## CLI 使用指南
130
131
 
@@ -390,18 +391,18 @@ MIT License,详见 [LICENSE](https://github.com/wangyuncepu/AISC/blob/main/LIC
390
391
 
391
392
  ### Codesome|Codex 与 Claude Code 二合一服务
392
393
 
393
- Codesome 提供 API 调用形式的二合一月卡:一张 `cr-...` API Key 可分别接入 Claude Code 和 Codex。这是 API 服务,不是 Claude 或 Codex 成品账号。
394
+ Codesome 提供 API 调用形式的接入服务,有两条产品线:**V3**(Claude/GPT 月卡 + 按量,`sk-...` Key,在后台建 Key 并选分组)与**二合一/V5**(月卡,订单里的 `cr-...` 卡密直接就是 Key,Claude/GPT 双端通用,无分组)。这是 API 服务,不是 Claude 或 Codex 成品账号。
394
395
 
395
- **v2.1.7 起 AISC 镜像已内置 Codesome-Group 预置供应商**——Workbench 的 Providers 页或容器内 cc-switch 选择 Codesome-Group、填入 API Key 即可使用,无需手工配置端点。
396
+ **2.1.12 起内置两个 Codesome 模板**(`codesome-v3` / `codesome-2in1`)——Workbench Providers 页「添加 provider」默认选中 Codesome,选对产品线、填入 Key 即可(表单下方「获取服务」直达注册/购买页)。两条线的地址不可混用,V3 的 `sk-` Key 不能配二合一,反之亦然。
396
397
 
397
- 手动配置时的端点(二合一 Key 两端通用,URL 不同不可混用):
398
+ 手动配置时的端点(两线各自两端通用,URL 不同不可混用):
398
399
 
399
- | 使用端 | API URL |
400
- | --- | --- |
401
- | Claude Code / Claude | `https://v5.codesome.cn/api` |
402
- | Codex / OpenAI 格式客户端 | `https://v5.codesome.cn/openai` |
400
+ | 产品线 | Claude 端 | Codex / OpenAI 格式端 |
401
+ | --- | --- | --- |
402
+ | V3(sk-) | `https://cc.codesome.ai` | `https://cc.codesome.ai/v1` |
403
+ | 二合一(cr-) | `https://v5.codesome.cn/api` | `https://v5.codesome.cn/openai` |
403
404
 
404
- Claude Code 通过 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN` 配置;Codex 使用 OpenAI 格式配置。完整教程见 [Codesome 文档](https://doc.codesome.ai/)。
405
+ Claude Code 通过 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN` 配置(加 `CLAUDE_CODE_ATTRIBUTION_HEADER=0`,官方核心三行之一);Codex 使用 OpenAI 格式配置(模型统一 `gpt-5.6-terra`)。可选:自动压缩(claude `CLAUDE_AUTO_COMPACT_WINDOW`,codex `model_auto_compact_token_limit`)到值自动压缩历史。完整教程见 [Codesome 文档](https://doc.codesome.ai/)。
405
406
 
406
407
  购买与开通(注册/下单入口,非服务使用地址):
407
408
 
@@ -1 +0,0 @@
1
- 0.1.1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes