@agentprojectcontext/apx 1.33.1 → 1.34.0
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.
- package/package.json +1 -1
- package/skills/apx/SKILL.md +49 -61
- package/src/core/agent/a2a/reply.js +48 -0
- package/src/core/agent/build-agent-system.js +4 -3
- package/src/core/agent/channels/voice-context.js +98 -0
- package/src/core/agent/memory.js +2 -1
- package/src/core/agent/prompt-builder.js +2 -1
- package/src/core/agent/prompts/modes/code-build.md +1 -0
- package/src/core/agent/prompts/modes/code-plan.md +1 -0
- package/src/core/agent/prompts/modes/index.js +28 -0
- package/src/core/agent/skills/loader.js +22 -18
- package/src/core/agent/stream/turn-accumulator.js +73 -0
- package/src/core/agent/suggestions.js +37 -0
- package/src/core/agent/tools/handlers/add-project.js +5 -2
- package/src/core/agent/tools/handlers/call-runtime.js +3 -2
- package/src/core/agent/tools/handlers/transcribe-audio.js +1 -1
- package/src/core/agent/tools/helpers.js +2 -2
- package/src/core/agent/tools/names.js +138 -0
- package/src/core/agent/tools/registry-bridge.js +6 -14
- package/src/core/agent/tools/registry.js +68 -65
- package/src/core/apc/context-copy.js +27 -0
- package/src/core/apc/notes.js +19 -0
- package/src/core/apc/parser.js +12 -5
- package/src/core/apc/paths.js +87 -0
- package/src/core/apc/scaffold.js +82 -76
- package/src/core/apc/skill-sync.js +10 -0
- package/src/{host/daemon/plugins → core/channels}/telegram/dispatch.js +38 -16
- package/src/core/config/index.js +3 -2
- package/src/core/config/redact.js +95 -0
- package/src/core/constants/channels.js +2 -0
- package/src/core/constants/code-modes.js +10 -0
- package/src/core/constants/index.js +1 -0
- package/src/core/deck/manifest.js +186 -0
- package/src/core/engines/catalog.js +83 -0
- package/src/core/{tools → http-tools}/browser.js +0 -1
- package/src/core/{tools → http-tools}/fetch.js +0 -1
- package/src/core/{tools → http-tools}/glob.js +0 -1
- package/src/core/{tools → http-tools}/grep.js +0 -1
- package/src/core/{tools → http-tools}/registry.js +0 -1
- package/src/core/{tools → http-tools}/search.js +0 -1
- package/src/core/i18n/en.js +9 -0
- package/src/core/i18n/es.js +12 -0
- package/src/core/i18n/index.js +54 -0
- package/src/core/i18n/pt.js +9 -0
- package/src/core/identity/telegram.js +2 -1
- package/src/core/mcp/runner.js +272 -14
- package/src/core/mcp/sources.js +3 -2
- package/src/core/routines/index.js +16 -0
- package/src/{host/daemon/routines.js → core/routines/runner.js} +36 -103
- package/src/core/runtime-skills/apc-context/SKILL.md +159 -0
- package/src/core/runtime-skills/apx/SKILL.md +95 -0
- package/src/core/runtime-skills/apx-mcp/SKILL.md +116 -0
- package/src/core/runtime-skills/{claude-code.md → claude-code/SKILL.md} +1 -0
- package/src/core/runtime-skills/{codex-cli.md → codex-cli/SKILL.md} +1 -0
- package/src/core/runtime-skills/{opencode-cli.md → opencode-cli/SKILL.md} +1 -0
- package/src/core/runtime-skills/{openrouter.md → openrouter/SKILL.md} +1 -0
- package/src/{host/daemon/env-detect.js → core/runtimes/detect.js} +1 -1
- package/src/core/stores/code-sessions.js +50 -2
- package/src/core/stores/routine-memory.js +1 -1
- package/src/core/stores/sessions-search.js +121 -0
- package/src/core/stores/sessions.js +38 -0
- package/src/core/vars/index.js +14 -0
- package/src/core/vars/interpolate.js +86 -0
- package/src/core/vars/sources.js +151 -0
- package/src/core/voice/audio-decode.js +38 -0
- package/src/core/voice/transcription.js +225 -0
- package/src/host/daemon/api/admin-config.js +5 -82
- package/src/host/daemon/api/agents.js +5 -5
- package/src/host/daemon/api/code.js +17 -169
- package/src/host/daemon/api/config.js +3 -4
- package/src/host/daemon/api/conversations.js +8 -29
- package/src/host/daemon/api/deck.js +37 -404
- package/src/host/daemon/api/engines.js +1 -80
- package/src/host/daemon/api/exec.js +1 -1
- package/src/host/daemon/api/mcps.js +32 -0
- package/src/host/daemon/api/routines.js +1 -1
- package/src/host/daemon/api/runtimes.js +4 -3
- package/src/host/daemon/api/sessions-search.js +24 -140
- package/src/host/daemon/api/sessions.js +12 -30
- package/src/host/daemon/api/shared.js +2 -1
- package/src/host/daemon/api/telegram.js +1 -11
- package/src/host/daemon/api/tools.js +6 -6
- package/src/host/daemon/api/transcribe.js +2 -2
- package/src/host/daemon/api/vars.js +137 -0
- package/src/host/daemon/api/voice.js +13 -290
- package/src/host/daemon/api.js +2 -0
- package/src/host/daemon/db.js +6 -6
- package/src/host/daemon/deck-exec.js +148 -0
- package/src/host/daemon/index.js +3 -3
- package/src/host/daemon/plugins/telegram/index.js +9 -9
- package/src/host/daemon/routines-scheduler.js +64 -0
- package/src/host/daemon/smoke.js +3 -2
- package/src/host/daemon/whisper-server.js +225 -0
- package/src/interfaces/cli/commands/agent.js +3 -2
- package/src/interfaces/cli/commands/command.js +2 -3
- package/src/interfaces/cli/commands/messages.js +6 -2
- package/src/interfaces/cli/commands/pair.js +5 -4
- package/src/interfaces/cli/commands/search.js +1 -1
- package/src/interfaces/cli/commands/sessions.js +3 -2
- package/src/interfaces/cli/commands/skills.js +36 -55
- package/src/interfaces/web/dist/assets/index-DdmSRtsz.css +1 -0
- package/src/interfaces/web/dist/assets/index-M4FspaCH.js +613 -0
- package/src/interfaces/web/dist/assets/index-M4FspaCH.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +182 -182
- package/src/interfaces/web/src/components/ModelCombobox.tsx +2 -1
- package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +1 -1
- package/src/interfaces/web/src/components/chat/AskAnswersCard.tsx +76 -0
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +16 -3
- package/src/interfaces/web/src/components/chat/MessageList.tsx +23 -1
- package/src/interfaces/web/src/components/chat/ModelPicker.tsx +3 -1
- package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +4 -4
- package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +1 -1
- package/src/interfaces/web/src/components/code/CodeFileTree.tsx +3 -2
- package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +3 -2
- package/src/interfaces/web/src/components/code/CodeTerminal.tsx +3 -2
- package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -1
- package/src/interfaces/web/src/components/deck/WidgetRow.tsx +2 -1
- package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +93 -0
- package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +449 -0
- package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +2 -1
- package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +2 -2
- package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +5 -4
- package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +3 -2
- package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +3 -2
- package/src/interfaces/web/src/components/ui/chat-input.tsx +5 -4
- package/src/interfaces/web/src/components/ui/sidebar.tsx +3 -2
- package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +2 -1
- package/src/interfaces/web/src/constants/index.ts +1 -1
- package/src/interfaces/web/src/i18n/en.ts +174 -7
- package/src/interfaces/web/src/i18n/es.ts +179 -15
- package/src/interfaces/web/src/lib/api/mcps.ts +25 -0
- package/src/interfaces/web/src/lib/api/vars.ts +38 -0
- package/src/interfaces/web/src/lib/api.ts +1 -0
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +8 -31
- package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +4 -3
- package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +7 -6
- package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +4 -3
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/project/ConfigTab.tsx +132 -1
- package/src/interfaces/web/src/screens/project/McpsTab.tsx +549 -104
- package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +1 -1
- package/src/interfaces/web/src/screens/project/VarsTab.tsx +300 -0
- package/src/interfaces/web/src/types/daemon.ts +5 -0
- package/src/host/daemon/transcription.js +0 -538
- package/src/host/daemon/whisper-transcribe.py +0 -73
- package/src/interfaces/web/dist/assets/index-Aaiw8BZN.css +0 -1
- package/src/interfaces/web/dist/assets/index-DPqtjDjh.js +0 -602
- package/src/interfaces/web/dist/assets/index-DPqtjDjh.js.map +0 -1
- /package/src/{host/daemon → core/apc}/projects-helpers.js +0 -0
- /package/src/{host/daemon/plugins → core/channels}/telegram/ask.js +0 -0
- /package/src/{host/daemon/plugins → core/channels}/telegram/helpers.js +0 -0
- /package/src/{host/daemon/plugins → core/channels}/telegram/media.js +0 -0
- /package/src/core/{tools → http-tools}/index.js +0 -0
- /package/{skills → src/core/runtime-skills}/apx-agency-agents/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-agent/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-mcp-builder/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-project/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-routine/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-runtime/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-sessions/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-skill-builder/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-task/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-telegram/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-voice/SKILL.md +0 -0
- /package/src/{host/daemon/compact.js → core/stores/conversations-compactor.js} +0 -0
- /package/src/{host/daemon → core/stores}/conversations.js +0 -0
- /package/src/{host/daemon → core/util}/thinking.js +0 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: apc-context
|
|
3
|
+
description: "ALWAYS activate when the project has a .apc/ directory or AGENTS.md file. Do not wait to be asked. Read .apc/ before making any assumption about agents, memory, or project structure. Activate on: .apc/, AGENTS.md, 'which agents', 'list agents', 'agent context', 'who are the agents', any question about agents or memory in this project. IMPORTANT: if .apc/migrate.md exists, open the conversation with a migration offer before answering anything else. If the user declines, delete .apc/migrate.md immediately so it is not shown again."
|
|
4
|
+
homepage: https://github.com/agentprojectcontext/agentprojectcontext
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Agent Project Context
|
|
8
|
+
|
|
9
|
+
This project uses APC. APC stores portable project context in `.apc/` and `AGENTS.md`.
|
|
10
|
+
|
|
11
|
+
APC does not store raw runtime sessions. Sessions, conversations, messages, caches, provider
|
|
12
|
+
threads, and private runtime memory stay in the IDE, CLI, daemon, or user-level store that created
|
|
13
|
+
them.
|
|
14
|
+
|
|
15
|
+
## FIRST: check for pending migration
|
|
16
|
+
|
|
17
|
+
Before doing anything else, check if `.apc/migrate.md` exists:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
cat .apc/migrate.md 2>/dev/null
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
If it exists, open with this offer before answering anything else:
|
|
24
|
+
|
|
25
|
+
> I see this project was initialized with Agent Project Context (APC).
|
|
26
|
+
>
|
|
27
|
+
> I found context files that may need migration:
|
|
28
|
+
> [list files from .apc/migrate.md]
|
|
29
|
+
>
|
|
30
|
+
> I can read them, separate durable project context from runtime/private state, and migrate only
|
|
31
|
+
> what belongs in APC.
|
|
32
|
+
>
|
|
33
|
+
> Want me to start?
|
|
34
|
+
|
|
35
|
+
If the user says no or later, delete `.apc/migrate.md` so the offer is not repeated.
|
|
36
|
+
|
|
37
|
+
## Migration rule: think, do not copy
|
|
38
|
+
|
|
39
|
+
Read detected files first. Also read `AGENTS.md` if it exists.
|
|
40
|
+
|
|
41
|
+
Classify content:
|
|
42
|
+
|
|
43
|
+
| Content | Action |
|
|
44
|
+
|---|---|
|
|
45
|
+
| Agent definitions: name, model, description | Put in `.apc/agents/<name>.md` and/or `AGENTS.md` |
|
|
46
|
+
| Shared project rules, stack notes, commands, testing policy | Keep in `AGENTS.md` |
|
|
47
|
+
| Reusable instruction blocks | Move to `.apc/skills/<name>.md` |
|
|
48
|
+
| Durable safe facts useful to all contributors | Add to `.apc/agents/<name>/memory.md` only after curation |
|
|
49
|
+
| MCP expectations without secrets | Add to `.apc/mcps.json` |
|
|
50
|
+
| Raw sessions, transcripts, conversations, messages, tool logs | Do not move into `.apc/`; leave with source runtime |
|
|
51
|
+
| Secrets, tokens, credentials, private headers | Do not store in repository |
|
|
52
|
+
| IDE UI settings or personal aliases | Leave in IDE/user config |
|
|
53
|
+
| Instructions to store sessions under `.apc/` | Drop as obsolete |
|
|
54
|
+
|
|
55
|
+
After migration:
|
|
56
|
+
|
|
57
|
+
1. Update `AGENTS.md` as the root project contract.
|
|
58
|
+
2. Create or update `.apc/agents/`, `.apc/skills/`, `.apc/mcps.json` as needed.
|
|
59
|
+
3. Do not create `.apc/**/sessions`, `.apc/messages`, or `.apc/conversations`.
|
|
60
|
+
4. Delete obsolete source files only when their useful project context was migrated or intentionally dropped.
|
|
61
|
+
5. Delete `.apc/migrate.md`.
|
|
62
|
+
6. Summarize what moved, what stayed local, and what was dropped.
|
|
63
|
+
|
|
64
|
+
## APC structure
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
AGENTS.md ← root project contract
|
|
68
|
+
.apc/
|
|
69
|
+
project.json ← project metadata
|
|
70
|
+
.gitignore ← safety guard
|
|
71
|
+
agents/<name>.md ← agent definition
|
|
72
|
+
agents/<name>/memory.md ← optional curated project memory
|
|
73
|
+
skills/<name>.md ← reusable project instructions
|
|
74
|
+
mcps.json ← MCP hints without secrets
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Do not store:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
.apc/agents/<name>/sessions/
|
|
81
|
+
.apc/sessions/
|
|
82
|
+
.apc/conversations/
|
|
83
|
+
.apc/messages/
|
|
84
|
+
.apc/cache/
|
|
85
|
+
.apc/tmp/
|
|
86
|
+
.apc/private/
|
|
87
|
+
.apc/secrets/
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Visibility rules
|
|
91
|
+
|
|
92
|
+
| Data | Visibility | Commit? |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| Agent definitions, skills, project rules | `stable` / `project` | Yes |
|
|
95
|
+
| Curated safe `memory.md` | `project` | Yes, if team-safe |
|
|
96
|
+
| MCP hints without secrets | `project` | Yes |
|
|
97
|
+
| Sessions, conversations, messages | `local` | No; runtime-owned |
|
|
98
|
+
| Secrets, tokens, `*.secret.json`, `*.env` | `private` | Never |
|
|
99
|
+
| Caches, temp files, databases | `ephemeral` | No |
|
|
100
|
+
|
|
101
|
+
## Operating rules
|
|
102
|
+
|
|
103
|
+
1. Read `AGENTS.md` and relevant `.apc/` files before assuming project context.
|
|
104
|
+
2. Read agent definitions from `.apc/agents/<name>.md` when present.
|
|
105
|
+
3. Read curated project memory from `.apc/agents/<name>/memory.md` when present.
|
|
106
|
+
4. Write only durable, safe, curated facts to APC memory.
|
|
107
|
+
5. Never write raw sessions, transcripts, messages, conversations, or tool logs into `.apc/`.
|
|
108
|
+
6. Keep secrets out of APC and out of git.
|
|
109
|
+
7. Treat `.apc/mcps.json` as MCP configuration hints, not as an MCP implementation.
|
|
110
|
+
|
|
111
|
+
## Normalization
|
|
112
|
+
|
|
113
|
+
If agent formats are broken or use legacy fields (role, skills in YAML), offer to normalize:
|
|
114
|
+
|
|
115
|
+
```yaml
|
|
116
|
+
---
|
|
117
|
+
name: agent-name
|
|
118
|
+
model: inherit
|
|
119
|
+
description: Semantic activation trigger
|
|
120
|
+
---
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Identify and fix inconsistencies in `model` (use technical IDs or `inherit`) and ensure `description` is present for semantic activation.
|
|
124
|
+
|
|
125
|
+
## Sessions
|
|
126
|
+
|
|
127
|
+
Sessions belong to the runtime that created them.
|
|
128
|
+
|
|
129
|
+
Examples:
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
Codex runtime storage
|
|
133
|
+
Claude Code runtime storage
|
|
134
|
+
OpenCode runtime storage
|
|
135
|
+
~/.apx/projects/<project-id>/agents/<name>/sessions/
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
At task end, provide the user a concise result. If project memory should be updated, write a short
|
|
139
|
+
sanitized fact to `.apc/agents/<name>/memory.md` only when useful and safe.
|
|
140
|
+
|
|
141
|
+
## APX
|
|
142
|
+
|
|
143
|
+
Read `.apc/project.json` if present. It may contain an `apx` field:
|
|
144
|
+
|
|
145
|
+
- `"installed"`: APX is available; use `apx` commands when useful.
|
|
146
|
+
- `"declined"`: user chose not to install; do not suggest or run APX.
|
|
147
|
+
- missing or `null`: unknown; do not assume APX is available.
|
|
148
|
+
|
|
149
|
+
If APX is installed, it may manage runtime state outside the repository:
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
~/.apx/projects/<project-id>/
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
APX can provide a local daemon, MCP management, Telegram bridge, routines, and runtime dispatch
|
|
156
|
+
across Codex, Claude Code, OpenCode, Aider, or direct LLM engines. Those are APX runtime features,
|
|
157
|
+
not APC portable-core requirements.
|
|
158
|
+
|
|
159
|
+
Never use APX to write secrets or raw sessions into `.apc/`.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: apx
|
|
3
|
+
description: "APX CLI umbrella — which sub-skill handles each operation (sessions, MCPs, routines, tasks, telegram, projects, agents, runtimes). Activate when the user mentions `apx`, the APX daemon, or wants to coordinate/run/delegate agents. Not for `.apc/` alone (use apc-context). Triggers: 'apx', 'apx run', 'apx daemon', 'coordinate agents', 'apx help'."
|
|
4
|
+
homepage: https://github.com/agentprojectcontext/apx
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# APX — Agent Project Context Runtime
|
|
8
|
+
|
|
9
|
+
APX is a daemon (`127.0.0.1:7430`, auto-starts on first call) that turns external coding CLIs (Claude Code, Codex, OpenCode, Aider, …) and configurable agents into a unified orchestration surface.
|
|
10
|
+
|
|
11
|
+
It reads APC project context from `.apc/` (committed) but keeps runtime state outside the repo under `~/.apx/projects/<project-id>/`. The super-agent has a default workspace at `~/.apx/projects/default` for system-level work.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## When to use APX (vs. spawning a subagent natively)
|
|
16
|
+
|
|
17
|
+
If you can spawn a subagent natively in the IDE you're in (Claude Code, Cursor, …) — **do that**. No APX needed.
|
|
18
|
+
|
|
19
|
+
Use APX when:
|
|
20
|
+
- The user explicitly asks for a specific external runtime ("run this in Codex", "delegate to OpenCode").
|
|
21
|
+
- You need to run an agent in a runtime different from the one you're in.
|
|
22
|
+
- You're orchestrating from outside any IDE (a script, Telegram bot, CI, routine).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Sub-skill index — open the one that matches the task
|
|
27
|
+
|
|
28
|
+
| Topic | Sub-skill | When |
|
|
29
|
+
|-------|-----------|------|
|
|
30
|
+
| Delegate to an external coding CLI | **apx-runtime** | `apx run <agent> --runtime claude-code\|codex\|...` |
|
|
31
|
+
| List / read / resume / summarise / continue sessions across engines | **apx-sessions** | `apx session resume`, `apx sessions list`, "import a codex session" |
|
|
32
|
+
| Use a registered MCP tool | **apx-mcp** | `apx mcp run`, "call MCP filesystem", "the MCP is failing" |
|
|
33
|
+
| Add / configure / use a project agent | **apx-agent** | "add an agent", "import from vault", per-agent model, agent memory |
|
|
34
|
+
| Register / list / configure a project | **apx-project** | "register this project", `apx project list`, per-project config |
|
|
35
|
+
| Per-project TODO list | **apx-task** | "add a task", "remind me to…", "what's pending" |
|
|
36
|
+
| Scheduled / recurring agents | **apx-routine** | `apx routine add`, every-5m, cron-like jobs |
|
|
37
|
+
| Telegram I/O | **apx-telegram** | configure bot, channels, send a message |
|
|
38
|
+
| Voice channel (TTS, speech) — *optional* | **apx-voice** | only if voice is being set up |
|
|
39
|
+
| Build a new MCP server — *internal/dev* | **apx-mcp-builder** | when developing a brand-new MCP from scratch |
|
|
40
|
+
| Author a new APX skill — *internal/dev* | **apx-skill-builder** | when adding to APX itself |
|
|
41
|
+
|
|
42
|
+
> Sub-skills marked *internal/dev* are not pushed to IDE skill dirs by default. They live in the APX repo and are loaded by APX itself; install one to your IDE with `apx skills add <slug> --global` if you want it there.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Generic patterns (apply to every sub-skill)
|
|
47
|
+
|
|
48
|
+
### Verify commands before recommending them
|
|
49
|
+
|
|
50
|
+
Do not invent APX subcommands. Confirm exact CLI form with `apx --help` or `apx <command> --help` before telling another runtime to invoke APX. Avoid guessed aliases (e.g. `apx send-telegram` is *not* a thing — see apx-telegram).
|
|
51
|
+
|
|
52
|
+
### `APC_RESULT` contract — structured return values
|
|
53
|
+
|
|
54
|
+
When you want APX to capture a structured value from an agent (any runtime), instruct the agent to print on its last meaningful line:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
APC_RESULT: <one-line value>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
APX's `extractApfResult()` parses that and stores it as the session's `result` field. Useful for automation, routines, and CI.
|
|
61
|
+
|
|
62
|
+
### Tool permissions
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
apx permission show
|
|
66
|
+
apx permission set automatico # total | automatico | permiso
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`automatico` runs read/list/safe shell checks directly and asks before destructive shell, MCP, runtime, outbound, config, or filesystem mutation actions.
|
|
70
|
+
|
|
71
|
+
### Memory
|
|
72
|
+
|
|
73
|
+
Write memory only for durable, safe project facts. Do not store raw transcripts or secrets.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
apx memory <slug> # read agent's memory.md
|
|
77
|
+
apx memory <slug> --append "<fact>" # append a durable note
|
|
78
|
+
apx memory <slug> --replace < file.md # replace entire memory from stdin
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Observe activity
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
apx messages tail # last 50 messages, all channels
|
|
85
|
+
apx messages chat --channel telegram -n 20 # readable chat view
|
|
86
|
+
apx messages tail --channel runtime --agent <slug> -n 20
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Anti-patterns
|
|
92
|
+
|
|
93
|
+
- Don't activate APX-sessions inside a request that's purely about `apx run` orchestration — use apx-runtime.
|
|
94
|
+
- Don't activate apx-mcp-builder unless the user is actually authoring a new MCP server (it's a deep developer guide, not a usage guide).
|
|
95
|
+
- Don't push state to `.apc/` that belongs in `~/.apx/projects/<id>/` (sessions, conversations, runtime logs).
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: apx-mcp
|
|
3
|
+
description: How to register, list, debug, and scope MCP servers in APX. Use BEFORE adding any MCP — three scopes (shared/runtime/global) with different commit and secrecy semantics.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# apx-mcp
|
|
7
|
+
|
|
8
|
+
APX exposes Model Context Protocol (MCP) servers to agents. Three scopes, each in a different file with different rules:
|
|
9
|
+
|
|
10
|
+
| Scope | File | Committed? | Secrets OK? | When |
|
|
11
|
+
|---|---|---|---|---|
|
|
12
|
+
| `shared` | `<repo>/.apc/mcps.json` | yes | **no** | Team-wide MCPs (filesystem, brave, github public) |
|
|
13
|
+
| `runtime` | `~/.apx/projects/<apxId>/mcps.json` (chmod 0600) | no | yes | Per-project local — tokens, machine-specific endpoints |
|
|
14
|
+
| `global` | `~/.apx/mcps.json` | n/a | yes | Machine-wide — not tied to any project |
|
|
15
|
+
|
|
16
|
+
Resolution priority when a name appears in more than one: **runtime > shared > global**. Conflicts surface in `apx mcp check`.
|
|
17
|
+
|
|
18
|
+
## Concrete CLI calls
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# List (all scopes, this is the default)
|
|
22
|
+
apx mcp list --project iacrmar
|
|
23
|
+
apx mcp list --scope runtime --project iacrmar
|
|
24
|
+
apx mcp list --scope shared --project iacrmar
|
|
25
|
+
apx mcp list --scope global
|
|
26
|
+
|
|
27
|
+
# Inspect sources and conflicts
|
|
28
|
+
apx mcp check --project iacrmar
|
|
29
|
+
|
|
30
|
+
# Add — shared (commit to repo)
|
|
31
|
+
apx mcp add filesystem --command npx --project iacrmar \
|
|
32
|
+
-- -y @modelcontextprotocol/server-filesystem .
|
|
33
|
+
|
|
34
|
+
# Add — runtime (per-project, local, secrets safe)
|
|
35
|
+
apx mcp add github --scope runtime --project iacrmar \
|
|
36
|
+
--command npx --env GITHUB_TOKEN=ghp_xxx \
|
|
37
|
+
-- -y @modelcontextprotocol/server-github
|
|
38
|
+
|
|
39
|
+
# Add — global (machine-wide, not tied to a project)
|
|
40
|
+
apx mcp add brave --scope global \
|
|
41
|
+
--command npx --env BRAVE_API_KEY=BSAxxx \
|
|
42
|
+
-- -y @modelcontextprotocol/server-brave-search
|
|
43
|
+
|
|
44
|
+
# Remove (pass --scope when the MCP isn't in the default scope:
|
|
45
|
+
# shared inside an APC project, else global)
|
|
46
|
+
apx mcp remove filesystem --project iacrmar
|
|
47
|
+
apx mcp remove github --scope runtime --project iacrmar
|
|
48
|
+
|
|
49
|
+
# Toggle (defaults to the scope that owns the MCP)
|
|
50
|
+
apx mcp enable filesystem --project iacrmar
|
|
51
|
+
apx mcp disable filesystem --project iacrmar
|
|
52
|
+
|
|
53
|
+
# Call a tool through the daemon (useful for debugging)
|
|
54
|
+
apx mcp run filesystem read_file '{"path":"README.md"}'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## When the user asks for a new MCP
|
|
58
|
+
|
|
59
|
+
Decision tree:
|
|
60
|
+
1. **Has secrets / tokens?** → `runtime` scope. Always.
|
|
61
|
+
2. **Is part of the project's shared dev environment?** → `shared` (committed).
|
|
62
|
+
3. **Used across all your projects?** → `global`.
|
|
63
|
+
|
|
64
|
+
Default if none is obvious: `shared` when inside an APC project, `global` outside.
|
|
65
|
+
|
|
66
|
+
## Common command shapes by transport
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# stdio MCP (most common — npx, uvx, node, python)
|
|
70
|
+
apx mcp add <name> --command npx -- -y <package-or-flag-list>
|
|
71
|
+
apx mcp add <name> --command uvx -- <python-cli-name>
|
|
72
|
+
apx mcp add <name> --command python -- /abs/path/to/server.py
|
|
73
|
+
|
|
74
|
+
# Env vars (one --env per var)
|
|
75
|
+
apx mcp add <name> --command npx \
|
|
76
|
+
--env GITHUB_TOKEN=ghp_xxx \
|
|
77
|
+
--env GITHUB_OWNER=manuel \
|
|
78
|
+
-- -y @modelcontextprotocol/server-github
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Anything after `--` is forwarded verbatim as args to the command. Quote carefully.
|
|
82
|
+
|
|
83
|
+
## Anti-examples
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# DON'T put tokens in shared scope. It commits.
|
|
87
|
+
apx mcp add github --scope shared --env GITHUB_TOKEN=ghp_xxx ...
|
|
88
|
+
# ↑ Token ends up in .apc/mcps.json in your repo. Use --scope runtime.
|
|
89
|
+
|
|
90
|
+
# DON'T remove an MCP from the wrong scope.
|
|
91
|
+
apx mcp remove github # if github lives in runtime, this errors with a hint
|
|
92
|
+
# ↑ Daemon returns 409 with the right scope to use.
|
|
93
|
+
|
|
94
|
+
# DON'T expect IDE-foreign configs (~/.cursor/mcps.json, ~/.claude/mcps.json) to be
|
|
95
|
+
# removable via apx mcp remove. APX reads them as advisory (source=cursor/claude/etc)
|
|
96
|
+
# but won't write them. Edit the IDE config directly.
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Debugging connection issues
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
apx mcp check --project iacrmar # what scopes APX sees + which files exist
|
|
103
|
+
apx mcp run <name> <tool> '{...}' # spawn the server and call a tool for real
|
|
104
|
+
apx log -f # tail unified log for spawn errors
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
A server that "doesn't show tools" usually means: the command failed to start (env vars missing, package not found), or the server crashed during initialize. The unified log has the stderr buffer.
|
|
108
|
+
|
|
109
|
+
> `apx mcp tools <name>` is a placeholder stub (prints a "coming in v0.2" notice, lists nothing). To verify a server actually spawns, call a tool with `apx mcp run`.
|
|
110
|
+
|
|
111
|
+
## Don't
|
|
112
|
+
|
|
113
|
+
- Don't mix scopes for the same MCP name unless you actually want shadowing. The result is "the one with highest priority wins, others stay invisible."
|
|
114
|
+
- Don't edit `~/.apx/projects/<id>/mcps.json` by hand; use `apx mcp add --scope runtime`. The file is chmod 0600 — the CLI keeps it that way.
|
|
115
|
+
- Don't add tokens via `--env KEY=` inline if your shell history is public. Set them in your shell first, then `--env KEY=$KEY`.
|
|
116
|
+
- Don't forget to `apx daemon reload` after editing config — actually `apx mcp` does this for you, but if you hand-edited the JSON, it's manual.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
scope: internal
|
|
2
3
|
name: claude-code
|
|
3
4
|
description: "Activate ONLY when the user explicitly mentions Claude Code, Claude CLI, claude command, Anthropic Claude Code, installing Claude Code, using Claude Code, or APX runtime claude-code. Do not activate for generic Claude model discussion."
|
|
4
5
|
homepage: https://docs.anthropic.com/en/docs/claude-code
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
scope: internal
|
|
2
3
|
name: codex-cli
|
|
3
4
|
description: "Activate ONLY when the user explicitly mentions Codex CLI, OpenAI Codex, @openai/codex, codex command, codex exec, installing Codex, using Codex, ~/.codex, or APX runtime codex."
|
|
4
5
|
homepage: https://developers.openai.com/codex
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
scope: internal
|
|
2
3
|
name: openrouter
|
|
3
4
|
description: "Activate ONLY when the user explicitly mentions OpenRouter, OPENROUTER_API_KEY, OpenRouter models, installing OpenRouter provider config, or using OpenRouter with APX, OpenCode, LiteLLM, or an OpenAI-compatible client."
|
|
4
5
|
homepage: https://openrouter.ai/docs
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Best-effort detection of installed agent CLIs and LLM runners.
|
|
2
2
|
// We just probe the binary with `--version` (or equivalent) and don't fail if
|
|
3
3
|
// it isn't there — caller decides what to do with absence.
|
|
4
|
-
import { runProcess } from "
|
|
4
|
+
import { runProcess } from "#host/daemon/runtimes/_spawn.js";
|
|
5
5
|
|
|
6
6
|
const PROBES = [
|
|
7
7
|
// Coding-agent CLIs (runtimes/)
|
|
@@ -13,6 +13,7 @@ import fs from "node:fs";
|
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { nowIso } from "../util/time.js";
|
|
15
15
|
import { shortId as makeShortId } from "../util/ids.js";
|
|
16
|
+
import { CODE_MODES, DEFAULT_CODE_MODE } from "../constants/code-modes.js";
|
|
16
17
|
|
|
17
18
|
function sessionsDir(storagePath) {
|
|
18
19
|
return path.join(storagePath, "code-sessions");
|
|
@@ -91,7 +92,7 @@ export function createCodeSession(storagePath, fields = {}) {
|
|
|
91
92
|
createdAt: ts,
|
|
92
93
|
updatedAt: ts,
|
|
93
94
|
model: fields.model || null,
|
|
94
|
-
mode: fields.mode ===
|
|
95
|
+
mode: fields.mode === CODE_MODES.PLAN ? CODE_MODES.PLAN : DEFAULT_CODE_MODE,
|
|
95
96
|
agentSlug: fields.agentSlug || null,
|
|
96
97
|
git: fields.git && typeof fields.git === "object" ? fields.git : null,
|
|
97
98
|
messages: [],
|
|
@@ -109,7 +110,7 @@ export function updateCodeSession(storagePath, id, patch = {}) {
|
|
|
109
110
|
if (!session) return null;
|
|
110
111
|
if (patch.title != null) session.title = String(patch.title).trim() || session.title;
|
|
111
112
|
if (patch.model !== undefined) session.model = patch.model || null;
|
|
112
|
-
if (patch.mode ===
|
|
113
|
+
if (patch.mode === CODE_MODES.PLAN || patch.mode === CODE_MODES.BUILD) session.mode = patch.mode;
|
|
113
114
|
if (patch.agentSlug !== undefined) session.agentSlug = patch.agentSlug || null;
|
|
114
115
|
if (patch.git !== undefined) session.git = patch.git;
|
|
115
116
|
session.updatedAt = nowIso();
|
|
@@ -147,3 +148,50 @@ export function appendTurn(storagePath, id, turn) {
|
|
|
147
148
|
writeJson(sessionFile(storagePath, id), session);
|
|
148
149
|
return session;
|
|
149
150
|
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Transcript → engine history
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
// One-line summary of an ask_questions tool call. Without it the next turn's
|
|
157
|
+
// history shows only "user answered X" with no record that the model had
|
|
158
|
+
// asked something — which makes the model ask again forever.
|
|
159
|
+
function summarizeAskQuestionsPart(part) {
|
|
160
|
+
const raw = part?.args?.questions;
|
|
161
|
+
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
162
|
+
const lines = raw
|
|
163
|
+
.map((q) => {
|
|
164
|
+
if (typeof q === "string") return `- ${q}`;
|
|
165
|
+
if (!q || typeof q !== "object" || typeof q.question !== "string") return null;
|
|
166
|
+
const opts = Array.isArray(q.options) ? q.options : [];
|
|
167
|
+
const optStr = opts
|
|
168
|
+
.map((o) => (typeof o === "string" ? o : (o && typeof o.label === "string" ? o.label : "")))
|
|
169
|
+
.filter(Boolean)
|
|
170
|
+
.join(", ");
|
|
171
|
+
return optStr ? `- ${q.question} (opciones: ${optStr})` : `- ${q.question}`;
|
|
172
|
+
})
|
|
173
|
+
.filter(Boolean);
|
|
174
|
+
if (lines.length === 0) return null;
|
|
175
|
+
return `[ask_questions]\n${lines.join("\n")}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Flatten a stored rich transcript into the [{role, content}] history the
|
|
180
|
+
* super-agent loop expects. Text parts are concatenated; tool parts are
|
|
181
|
+
* normally internal, except ask_questions which is surfaced as a one-line
|
|
182
|
+
* summary so the model doesn't lose track of what it already asked.
|
|
183
|
+
*/
|
|
184
|
+
export function codeSessionHistory(session) {
|
|
185
|
+
return (session?.messages || []).map((m) => {
|
|
186
|
+
const chunks = [];
|
|
187
|
+
for (const p of m.parts || []) {
|
|
188
|
+
if (!p) continue;
|
|
189
|
+
if (p.kind === "text" && p.text) chunks.push(p.text);
|
|
190
|
+
else if (p.kind === "tool" && p.tool === "ask_questions") {
|
|
191
|
+
const summary = summarizeAskQuestionsPart(p);
|
|
192
|
+
if (summary) chunks.push(summary);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { role: m.role, content: chunks.join("\n\n").trim() };
|
|
196
|
+
});
|
|
197
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Path: <projectStoragePath>/routines/<routineId>/memory.md
|
|
4
4
|
//
|
|
5
|
-
// The routine handler (
|
|
5
|
+
// The routine handler (core/routines/runner.js) creates the file on first read
|
|
6
6
|
// and injects a bounded slice into the super-agent prompt via
|
|
7
7
|
// channelMeta.routineMemory. The routine can write back with future tooling;
|
|
8
8
|
// today we only read.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Cross-agent, cross-conversation session search + locator.
|
|
2
|
+
// Walks the on-disk session and conversation files for each project and
|
|
3
|
+
// returns matches with a small excerpt window. Used by the HTTP adapter and
|
|
4
|
+
// (planned) CLI session find.
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { apcAgentsDir } from "../apc/paths.js";
|
|
8
|
+
|
|
9
|
+
const EXCERPT_CHARS = 300;
|
|
10
|
+
const EXCERPT_LINES_BEFORE = 1;
|
|
11
|
+
const EXCERPT_LINES_AFTER = 3;
|
|
12
|
+
|
|
13
|
+
function scanFile(filePath, needle) {
|
|
14
|
+
try {
|
|
15
|
+
const text = fs.readFileSync(filePath, "utf8");
|
|
16
|
+
if (!text.toLowerCase().includes(needle)) return null;
|
|
17
|
+
const lines = text.split("\n");
|
|
18
|
+
const matchLine = lines.findIndex((l) => l.toLowerCase().includes(needle));
|
|
19
|
+
const excerpt = lines
|
|
20
|
+
.slice(Math.max(0, matchLine - EXCERPT_LINES_BEFORE), matchLine + EXCERPT_LINES_AFTER)
|
|
21
|
+
.join("\n");
|
|
22
|
+
return excerpt.slice(0, EXCERPT_CHARS);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Search for `needle` across one project's session + conversation files.
|
|
30
|
+
*
|
|
31
|
+
* @param project { id, path, storagePath } record from ProjectManager
|
|
32
|
+
* @param needle lowercase query string
|
|
33
|
+
* @param remaining max matches to add (search short-circuits when reached)
|
|
34
|
+
* @returns matches array (may be empty)
|
|
35
|
+
*/
|
|
36
|
+
export function searchProjectSessions(project, needle, remaining) {
|
|
37
|
+
const matches = [];
|
|
38
|
+
if (!project || remaining <= 0) return matches;
|
|
39
|
+
|
|
40
|
+
// 1) Legacy session files in the repo (.apc/agents/<slug>/sessions/)
|
|
41
|
+
const sessionAgentsDir = apcAgentsDir(project.path);
|
|
42
|
+
if (fs.existsSync(sessionAgentsDir)) {
|
|
43
|
+
for (const slug of fs.readdirSync(sessionAgentsDir)) {
|
|
44
|
+
const sessionsDir = path.join(sessionAgentsDir, slug, "sessions");
|
|
45
|
+
if (!fs.existsSync(sessionsDir)) continue;
|
|
46
|
+
for (const f of fs.readdirSync(sessionsDir).filter((x) => x.endsWith(".md"))) {
|
|
47
|
+
const filePath = path.join(sessionsDir, f);
|
|
48
|
+
const excerpt = scanFile(filePath, needle);
|
|
49
|
+
if (excerpt != null) {
|
|
50
|
+
matches.push({
|
|
51
|
+
type: "session",
|
|
52
|
+
project: project.id,
|
|
53
|
+
agent: slug,
|
|
54
|
+
filename: f,
|
|
55
|
+
path: filePath,
|
|
56
|
+
excerpt,
|
|
57
|
+
});
|
|
58
|
+
if (matches.length >= remaining) return matches;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 2) Conversation files in daemon storage (~/.apx/projects/<id>/agents/<slug>/conversations/)
|
|
65
|
+
const convAgentsDir = path.join(project.storagePath, "agents");
|
|
66
|
+
if (fs.existsSync(convAgentsDir)) {
|
|
67
|
+
for (const slug of fs.readdirSync(convAgentsDir)) {
|
|
68
|
+
const convDir = path.join(convAgentsDir, slug, "conversations");
|
|
69
|
+
if (!fs.existsSync(convDir)) continue;
|
|
70
|
+
for (const f of fs.readdirSync(convDir).filter((x) => x.endsWith(".md"))) {
|
|
71
|
+
const filePath = path.join(convDir, f);
|
|
72
|
+
const excerpt = scanFile(filePath, needle);
|
|
73
|
+
if (excerpt != null) {
|
|
74
|
+
matches.push({
|
|
75
|
+
type: "conversation",
|
|
76
|
+
project: project.id,
|
|
77
|
+
agent: slug,
|
|
78
|
+
filename: f,
|
|
79
|
+
path: filePath,
|
|
80
|
+
excerpt,
|
|
81
|
+
});
|
|
82
|
+
if (matches.length >= remaining) return matches;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return matches;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Run searchProjectSessions across an array of projects, capping at `limit`. */
|
|
92
|
+
export function searchSessions(projectList, query, limit) {
|
|
93
|
+
const needle = String(query || "").toLowerCase();
|
|
94
|
+
const matches = [];
|
|
95
|
+
for (const p of projectList) {
|
|
96
|
+
if (!p) continue;
|
|
97
|
+
const remaining = limit - matches.length;
|
|
98
|
+
if (remaining <= 0) break;
|
|
99
|
+
matches.push(...searchProjectSessions(p, needle, remaining));
|
|
100
|
+
}
|
|
101
|
+
return matches.slice(0, limit);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Find the conversation file (under daemon storage) for a given session id,
|
|
106
|
+
* scanning a list of candidate projects. Returns { project, agentSlug, filename }
|
|
107
|
+
* or null. `id` is taken as bare or with .md suffix.
|
|
108
|
+
*/
|
|
109
|
+
export function findSessionFile(projectList, id) {
|
|
110
|
+
const filename = id.endsWith(".md") ? id : `${id}.md`;
|
|
111
|
+
for (const p of projectList) {
|
|
112
|
+
if (!p) continue;
|
|
113
|
+
const agentsDir = path.join(p.storagePath, "agents");
|
|
114
|
+
if (!fs.existsSync(agentsDir)) continue;
|
|
115
|
+
for (const slug of fs.readdirSync(agentsDir)) {
|
|
116
|
+
const f = path.join(agentsDir, slug, "conversations", filename);
|
|
117
|
+
if (fs.existsSync(f)) return { project: p, agentSlug: slug, filename };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { nowIso } from "../util/time.js";
|
|
8
|
+
|
|
9
|
+
export function agentSessionsDir(storageRoot, agentSlug) {
|
|
10
|
+
return path.join(storageRoot, "agents", agentSlug, "sessions");
|
|
11
|
+
}
|
|
7
12
|
|
|
8
13
|
export function generateSessionId(storageRoot, agentSlug) {
|
|
9
14
|
const today = new Date().toISOString().slice(0, 10);
|
|
@@ -34,3 +39,36 @@ export function readSessionFrontmatter(filePath) {
|
|
|
34
39
|
}
|
|
35
40
|
return { fm, body: text.slice(end + 4).replace(/^\n+/, "") };
|
|
36
41
|
}
|
|
42
|
+
|
|
43
|
+
function slugifyTitle(title) {
|
|
44
|
+
return (
|
|
45
|
+
String(title || "")
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
48
|
+
.replace(/^-|-$/g, "") || "session"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a new dated session file under
|
|
54
|
+
* `<storageRoot>/agents/<agentSlug>/sessions/YYYY-MM-DD-<titleSlug>.md`,
|
|
55
|
+
* with collision suffix (`-2`, `-3`, …) and standard frontmatter.
|
|
56
|
+
* Returns { filename, path, started }.
|
|
57
|
+
*/
|
|
58
|
+
export function createAgentSessionFile(storageRoot, agentSlug, { title, body = "" }) {
|
|
59
|
+
if (!title) throw new Error("createAgentSessionFile: title required");
|
|
60
|
+
const dir = agentSessionsDir(storageRoot, agentSlug);
|
|
61
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
62
|
+
const titleSlug = slugifyTitle(title);
|
|
63
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
64
|
+
let candidate = path.join(dir, `${today}-${titleSlug}.md`);
|
|
65
|
+
let n = 2;
|
|
66
|
+
while (fs.existsSync(candidate)) {
|
|
67
|
+
candidate = path.join(dir, `${today}-${titleSlug}-${n}.md`);
|
|
68
|
+
n++;
|
|
69
|
+
}
|
|
70
|
+
const started = nowIso();
|
|
71
|
+
const content = `---\ntitle: ${title}\nstarted: ${started}\n---\n\n# ${title}\n\n${body}\n`;
|
|
72
|
+
fs.writeFileSync(candidate, content);
|
|
73
|
+
return { filename: path.basename(candidate), path: candidate, started };
|
|
74
|
+
}
|