projectmemory-cli 0.1.0__py3-none-any.whl
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.
- projectmemory_cli/__init__.py +3 -0
- projectmemory_cli/api/__init__.py +1 -0
- projectmemory_cli/api/auth.py +94 -0
- projectmemory_cli/api/client.py +135 -0
- projectmemory_cli/api/errors.py +124 -0
- projectmemory_cli/api/sessions.py +25 -0
- projectmemory_cli/app.py +80 -0
- projectmemory_cli/commands/__init__.py +1 -0
- projectmemory_cli/commands/auth.py +163 -0
- projectmemory_cli/commands/commits.py +147 -0
- projectmemory_cli/commands/config.py +56 -0
- projectmemory_cli/commands/doctor.py +375 -0
- projectmemory_cli/commands/issues.py +234 -0
- projectmemory_cli/commands/memory.py +235 -0
- projectmemory_cli/commands/next_action.py +282 -0
- projectmemory_cli/commands/notes.py +231 -0
- projectmemory_cli/commands/projects.py +325 -0
- projectmemory_cli/commands/resume.py +240 -0
- projectmemory_cli/commands/sessions.py +277 -0
- projectmemory_cli/commands/tasks.py +435 -0
- projectmemory_cli/config.py +101 -0
- projectmemory_cli/credentials.py +98 -0
- projectmemory_cli/git.py +124 -0
- projectmemory_cli/main.py +4 -0
- projectmemory_cli/models/__init__.py +1 -0
- projectmemory_cli/models/api.py +226 -0
- projectmemory_cli/output.py +227 -0
- projectmemory_cli/project_context.py +244 -0
- projectmemory_cli-0.1.0.dist-info/METADATA +239 -0
- projectmemory_cli-0.1.0.dist-info/RECORD +32 -0
- projectmemory_cli-0.1.0.dist-info/WHEEL +4 -0
- projectmemory_cli-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Next Action commands: pm next, pm next set, pm next task, pm next issue, pm next clear."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import questionary
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from projectmemory_cli import output
|
|
10
|
+
from projectmemory_cli.api.client import APIClient
|
|
11
|
+
from projectmemory_cli.api.errors import APIError
|
|
12
|
+
from projectmemory_cli.project_context import guard_project_session
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(help="Next Action commands.", no_args_is_help=False)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _require_client() -> APIClient:
|
|
18
|
+
from projectmemory_cli import credentials
|
|
19
|
+
token = credentials.get_token()
|
|
20
|
+
if not token:
|
|
21
|
+
output.error("Not logged in. Run: pm login")
|
|
22
|
+
raise SystemExit(2)
|
|
23
|
+
return APIClient.from_env()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _session_client(client: APIClient, session_id: int) -> APIClient:
|
|
27
|
+
return APIClient(base_url=client.base_url, token=client.token, session_id=session_id, debug=client.debug)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _display_next_action(na: dict) -> None:
|
|
31
|
+
title = na.get("title", "")
|
|
32
|
+
source_type = na.get("source_type", "manual")
|
|
33
|
+
linked = na.get("linked_source")
|
|
34
|
+
|
|
35
|
+
if not title:
|
|
36
|
+
output.info("[dim](no next action set)[/dim]")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
output.blank()
|
|
40
|
+
output.header("Next Action")
|
|
41
|
+
output.info(f" {title}")
|
|
42
|
+
if source_type == "manual":
|
|
43
|
+
output.hint(" Manual")
|
|
44
|
+
elif linked:
|
|
45
|
+
t = linked.get("type", source_type)
|
|
46
|
+
lid = linked.get("id", "")
|
|
47
|
+
ltitle = linked.get("title", "")
|
|
48
|
+
output.hint(f" Linked {t} #{lid}: {ltitle}")
|
|
49
|
+
else:
|
|
50
|
+
output.hint(f" Source: {source_type}")
|
|
51
|
+
output.blank()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def run_next_action_menu(client: APIClient, project, session, *, project_arg: str | None = None) -> bool:
|
|
55
|
+
"""Run the same unified Next Action choices used by web, via CLI orchestration.
|
|
56
|
+
|
|
57
|
+
Returns True when a backend change was attempted successfully.
|
|
58
|
+
"""
|
|
59
|
+
session_client = _session_client(client, session.id)
|
|
60
|
+
choice = questionary.select(
|
|
61
|
+
"Next Action:",
|
|
62
|
+
choices=[
|
|
63
|
+
questionary.Choice("Write Next Action", value="write"),
|
|
64
|
+
questionary.Choice("Create and link a task", value="create_task"),
|
|
65
|
+
questionary.Choice("Link an existing task", value="link_task"),
|
|
66
|
+
questionary.Choice("Link an open issue", value="link_issue"),
|
|
67
|
+
questionary.Choice("Clear Next Action", value="clear"),
|
|
68
|
+
questionary.Choice("Cancel", value="cancel"),
|
|
69
|
+
],
|
|
70
|
+
).ask()
|
|
71
|
+
|
|
72
|
+
if not choice or choice == "cancel":
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
if choice == "write":
|
|
77
|
+
text = questionary.text("Next Action:").ask()
|
|
78
|
+
if not text:
|
|
79
|
+
return False
|
|
80
|
+
data = session_client.patch(
|
|
81
|
+
f"/api/cli/projects/{project.id}/next-action/",
|
|
82
|
+
json={"title": text},
|
|
83
|
+
project_name=project.name,
|
|
84
|
+
)
|
|
85
|
+
output.success(f"Next Action set: [bold]{text}[/bold]")
|
|
86
|
+
_display_next_action(data.get("next_action", {}))
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
if choice == "create_task":
|
|
90
|
+
title = questionary.text("Task title:").ask()
|
|
91
|
+
if not title:
|
|
92
|
+
return False
|
|
93
|
+
task_data = session_client.post(
|
|
94
|
+
f"/api/cli/projects/{project.id}/tasks/",
|
|
95
|
+
json={"title": title},
|
|
96
|
+
project_name=project.name,
|
|
97
|
+
)
|
|
98
|
+
task_id = task_data["task"]["id"]
|
|
99
|
+
data = session_client.post(
|
|
100
|
+
f"/api/cli/projects/{project.id}/next-action/task/",
|
|
101
|
+
json={"task_id": task_id},
|
|
102
|
+
project_name=project.name,
|
|
103
|
+
)
|
|
104
|
+
output.success(f"Task #{task_id} created and set as Next Action.")
|
|
105
|
+
_display_next_action(data.get("next_action", {}))
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
if choice == "link_task":
|
|
109
|
+
tasks_data = client.get(f"/api/cli/projects/{project.id}/tasks/", params={"status": "open"})
|
|
110
|
+
tasks = tasks_data.get("tasks", [])
|
|
111
|
+
if not tasks:
|
|
112
|
+
output.warn("No open tasks.")
|
|
113
|
+
return False
|
|
114
|
+
task = questionary.select(
|
|
115
|
+
"Select task:",
|
|
116
|
+
choices=[questionary.Choice(f"#{t['id']} {t['title']}", value=t) for t in tasks],
|
|
117
|
+
).ask()
|
|
118
|
+
if not task:
|
|
119
|
+
return False
|
|
120
|
+
data = session_client.post(
|
|
121
|
+
f"/api/cli/projects/{project.id}/next-action/task/",
|
|
122
|
+
json={"task_id": task["id"]},
|
|
123
|
+
project_name=project.name,
|
|
124
|
+
)
|
|
125
|
+
output.success(f"Task #{task['id']} set as Next Action.")
|
|
126
|
+
_display_next_action(data.get("next_action", {}))
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
if choice == "link_issue":
|
|
130
|
+
issues_data = client.get(f"/api/cli/projects/{project.id}/issues/", params={"status": "open"})
|
|
131
|
+
issues = issues_data.get("issues", [])
|
|
132
|
+
if not issues:
|
|
133
|
+
output.warn("No open issues.")
|
|
134
|
+
return False
|
|
135
|
+
issue = questionary.select(
|
|
136
|
+
"Select issue:",
|
|
137
|
+
choices=[questionary.Choice(f"#{i['id']} {i['description']}", value=i) for i in issues],
|
|
138
|
+
).ask()
|
|
139
|
+
if not issue:
|
|
140
|
+
return False
|
|
141
|
+
data = session_client.post(
|
|
142
|
+
f"/api/cli/projects/{project.id}/next-action/issue/",
|
|
143
|
+
json={"issue_id": issue["id"]},
|
|
144
|
+
project_name=project.name,
|
|
145
|
+
)
|
|
146
|
+
output.success(f"Issue #{issue['id']} set as Next Action.")
|
|
147
|
+
_display_next_action(data.get("next_action", {}))
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
if choice == "clear":
|
|
151
|
+
data = session_client.post(
|
|
152
|
+
f"/api/cli/projects/{project.id}/next-action/clear/",
|
|
153
|
+
project_name=project.name,
|
|
154
|
+
)
|
|
155
|
+
output.success("Next Action cleared.")
|
|
156
|
+
_display_next_action(data.get("next_action", {}))
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
except APIError as exc:
|
|
160
|
+
output.error(str(exc))
|
|
161
|
+
raise SystemExit(exc.exit_code)
|
|
162
|
+
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@app.callback(invoke_without_command=True)
|
|
167
|
+
def next_default(
|
|
168
|
+
ctx: typer.Context,
|
|
169
|
+
project_arg: Optional[str] = typer.Option(None, "--project", "-p"),
|
|
170
|
+
) -> None:
|
|
171
|
+
"""Show the current Next Action when no subcommand is supplied."""
|
|
172
|
+
if ctx.invoked_subcommand is not None:
|
|
173
|
+
return
|
|
174
|
+
next_show(project_arg=project_arg)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@app.command("show")
|
|
178
|
+
def next_show(
|
|
179
|
+
project_arg: Optional[str] = typer.Option(None, "--project", "-p"),
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Show the current Next Action."""
|
|
182
|
+
client = _require_client()
|
|
183
|
+
project, _session = guard_project_session(client, project_arg=project_arg)
|
|
184
|
+
try:
|
|
185
|
+
data = client.get(f"/api/cli/projects/{project.id}/next-action/")
|
|
186
|
+
except APIError as exc:
|
|
187
|
+
output.error(str(exc))
|
|
188
|
+
raise SystemExit(exc.exit_code)
|
|
189
|
+
_display_next_action(data.get("next_action", {}))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@app.command("set")
|
|
193
|
+
def next_set(
|
|
194
|
+
text: Optional[str] = typer.Argument(None, help="Next Action text. Omit for interactive menu."),
|
|
195
|
+
project_arg: Optional[str] = typer.Option(None, "--project", "-p"),
|
|
196
|
+
) -> None:
|
|
197
|
+
"""Set the Next Action manually or use the unified interactive menu."""
|
|
198
|
+
client = _require_client()
|
|
199
|
+
project, session = guard_project_session(client, project_arg=project_arg)
|
|
200
|
+
session_client = _session_client(client, session.id)
|
|
201
|
+
|
|
202
|
+
if text:
|
|
203
|
+
try:
|
|
204
|
+
data = session_client.patch(
|
|
205
|
+
f"/api/cli/projects/{project.id}/next-action/",
|
|
206
|
+
json={"title": text},
|
|
207
|
+
project_name=project.name,
|
|
208
|
+
)
|
|
209
|
+
except APIError as exc:
|
|
210
|
+
output.error(str(exc))
|
|
211
|
+
raise SystemExit(exc.exit_code)
|
|
212
|
+
output.success(f"Next Action set: [bold]{text}[/bold]")
|
|
213
|
+
_display_next_action(data.get("next_action", {}))
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
changed = run_next_action_menu(client, project, session, project_arg=project_arg)
|
|
217
|
+
if not changed:
|
|
218
|
+
output.info("No changes made.")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@app.command("task")
|
|
222
|
+
def next_task(
|
|
223
|
+
task_id: int = typer.Argument(..., help="Task ID to set as Next Action."),
|
|
224
|
+
project_arg: Optional[str] = typer.Option(None, "--project", "-p"),
|
|
225
|
+
) -> None:
|
|
226
|
+
"""Link a task as the Next Action."""
|
|
227
|
+
client = _require_client()
|
|
228
|
+
project, session = guard_project_session(client, project_arg=project_arg)
|
|
229
|
+
session_client = _session_client(client, session.id)
|
|
230
|
+
try:
|
|
231
|
+
data = session_client.post(
|
|
232
|
+
f"/api/cli/projects/{project.id}/next-action/task/",
|
|
233
|
+
json={"task_id": task_id},
|
|
234
|
+
project_name=project.name,
|
|
235
|
+
)
|
|
236
|
+
except APIError as exc:
|
|
237
|
+
output.error(str(exc))
|
|
238
|
+
raise SystemExit(exc.exit_code)
|
|
239
|
+
output.success(f"Task #{task_id} set as Next Action.")
|
|
240
|
+
_display_next_action(data.get("next_action", {}))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@app.command("issue")
|
|
244
|
+
def next_issue(
|
|
245
|
+
issue_id: int = typer.Argument(..., help="Issue ID to set as Next Action."),
|
|
246
|
+
project_arg: Optional[str] = typer.Option(None, "--project", "-p"),
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Link an issue as the Next Action."""
|
|
249
|
+
client = _require_client()
|
|
250
|
+
project, session = guard_project_session(client, project_arg=project_arg)
|
|
251
|
+
session_client = _session_client(client, session.id)
|
|
252
|
+
try:
|
|
253
|
+
data = session_client.post(
|
|
254
|
+
f"/api/cli/projects/{project.id}/next-action/issue/",
|
|
255
|
+
json={"issue_id": issue_id},
|
|
256
|
+
project_name=project.name,
|
|
257
|
+
)
|
|
258
|
+
except APIError as exc:
|
|
259
|
+
output.error(str(exc))
|
|
260
|
+
raise SystemExit(exc.exit_code)
|
|
261
|
+
output.success(f"Issue #{issue_id} set as Next Action.")
|
|
262
|
+
_display_next_action(data.get("next_action", {}))
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@app.command("clear")
|
|
266
|
+
def next_clear(
|
|
267
|
+
project_arg: Optional[str] = typer.Option(None, "--project", "-p"),
|
|
268
|
+
) -> None:
|
|
269
|
+
"""Clear the Next Action."""
|
|
270
|
+
client = _require_client()
|
|
271
|
+
project, session = guard_project_session(client, project_arg=project_arg)
|
|
272
|
+
session_client = _session_client(client, session.id)
|
|
273
|
+
try:
|
|
274
|
+
data = session_client.post(
|
|
275
|
+
f"/api/cli/projects/{project.id}/next-action/clear/",
|
|
276
|
+
project_name=project.name,
|
|
277
|
+
)
|
|
278
|
+
except APIError as exc:
|
|
279
|
+
output.error(str(exc))
|
|
280
|
+
raise SystemExit(exc.exit_code)
|
|
281
|
+
output.success("Next Action cleared.")
|
|
282
|
+
_display_next_action(data.get("next_action", {}))
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Note commands: pm note list/show/add/edit/delete, pm capture."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import questionary
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from projectmemory_cli import output
|
|
11
|
+
from projectmemory_cli.api.client import APIClient
|
|
12
|
+
from projectmemory_cli.api.errors import APIError
|
|
13
|
+
from projectmemory_cli.models.api import Note
|
|
14
|
+
from projectmemory_cli.project_context import guard_project_session
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help="Note commands.", no_args_is_help=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _require_client() -> APIClient:
|
|
20
|
+
from projectmemory_cli import credentials
|
|
21
|
+
token = credentials.get_token()
|
|
22
|
+
if not token:
|
|
23
|
+
output.error("Not logged in. Run: pm login")
|
|
24
|
+
raise SystemExit(2)
|
|
25
|
+
return APIClient.from_env()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _session_client(client: APIClient, session_id: int) -> APIClient:
|
|
29
|
+
return APIClient(base_url=client.base_url, token=client.token, session_id=session_id)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _display_note(note: Note) -> None:
|
|
33
|
+
output.blank()
|
|
34
|
+
if note.title:
|
|
35
|
+
output.info(f"[bold]#{note.id} {note.title}[/bold]")
|
|
36
|
+
else:
|
|
37
|
+
output.info(f"[bold]#{note.id}[/bold]")
|
|
38
|
+
output.info(note.content)
|
|
39
|
+
output.hint(f"Updated {output.format_ago(note.updated_at)}")
|
|
40
|
+
output.blank()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.command("list")
|
|
44
|
+
def note_list(
|
|
45
|
+
search: Optional[str] = typer.Option(None, "--search", help="Search query."),
|
|
46
|
+
json: bool = typer.Option(False, "--json"),
|
|
47
|
+
project_arg: Optional[str] = typer.Option(None, "--project", "-p"),
|
|
48
|
+
) -> None:
|
|
49
|
+
"""List notes for the current project."""
|
|
50
|
+
output.configure(json_mode=json)
|
|
51
|
+
client = _require_client()
|
|
52
|
+
project, session = guard_project_session(client, project_arg=project_arg)
|
|
53
|
+
|
|
54
|
+
params: dict = {}
|
|
55
|
+
if search:
|
|
56
|
+
params["search"] = search
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
data = client.get(f"/api/cli/projects/{project.id}/notes/", params=params)
|
|
60
|
+
except APIError as exc:
|
|
61
|
+
output.error(str(exc))
|
|
62
|
+
raise SystemExit(exc.exit_code)
|
|
63
|
+
|
|
64
|
+
if json:
|
|
65
|
+
output.print_json(data)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
notes = [Note.model_validate(n) for n in data.get("notes", [])]
|
|
69
|
+
if not notes:
|
|
70
|
+
output.hint("No notes.")
|
|
71
|
+
return
|
|
72
|
+
headers = ["#", "Title", "Updated"]
|
|
73
|
+
rows = [[str(n.id), n.title or n.content[:50], output.format_ago(n.updated_at)] for n in notes]
|
|
74
|
+
output.table(headers, rows, title=f"{project.name} — Notes ({data.get('count', len(notes))})")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@app.command("show")
|
|
78
|
+
def note_show(
|
|
79
|
+
note_id: int = typer.Argument(...),
|
|
80
|
+
json: bool = typer.Option(False, "--json"),
|
|
81
|
+
project_arg: Optional[str] = typer.Option(None, "--project", "-p"),
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Show a note's full content."""
|
|
84
|
+
output.configure(json_mode=json)
|
|
85
|
+
client = _require_client()
|
|
86
|
+
guard_project_session(client, project_arg=project_arg)
|
|
87
|
+
try:
|
|
88
|
+
data = client.get(f"/api/cli/notes/{note_id}/")
|
|
89
|
+
except APIError as exc:
|
|
90
|
+
output.error(str(exc))
|
|
91
|
+
raise SystemExit(exc.exit_code)
|
|
92
|
+
if json:
|
|
93
|
+
output.print_json(data)
|
|
94
|
+
return
|
|
95
|
+
note = Note.model_validate(data.get("note", {}))
|
|
96
|
+
_display_note(note)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@app.command("add")
|
|
100
|
+
def note_add(
|
|
101
|
+
content: Optional[str] = typer.Argument(None, help="Note content."),
|
|
102
|
+
title: Optional[str] = typer.Option(None, "--title", "-t", help="Note title."),
|
|
103
|
+
editor: bool = typer.Option(False, "--editor", "-e", help="Open editor for note content."),
|
|
104
|
+
stdin: bool = typer.Option(False, "--stdin", help="Read content from stdin."),
|
|
105
|
+
project_arg: Optional[str] = typer.Option(None, "--project", "-p"),
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Add a note to the current project."""
|
|
108
|
+
client = _require_client()
|
|
109
|
+
project, session = guard_project_session(client, project_arg=project_arg)
|
|
110
|
+
sc = _session_client(client, session.id)
|
|
111
|
+
|
|
112
|
+
if stdin or (not content and not sys.stdin.isatty()):
|
|
113
|
+
content = sys.stdin.read().strip()
|
|
114
|
+
elif editor:
|
|
115
|
+
from projectmemory_cli.commands.memory import _open_in_editor
|
|
116
|
+
content = _open_in_editor(content or "")
|
|
117
|
+
elif not content:
|
|
118
|
+
content = questionary.text("Note content:").ask() or ""
|
|
119
|
+
|
|
120
|
+
if not content:
|
|
121
|
+
output.info("No content provided.")
|
|
122
|
+
raise SystemExit(0)
|
|
123
|
+
|
|
124
|
+
if not title:
|
|
125
|
+
title = ""
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
data = sc.post(
|
|
129
|
+
f"/api/cli/projects/{project.id}/notes/",
|
|
130
|
+
json={"content": content, "title": title},
|
|
131
|
+
project_name=project.name,
|
|
132
|
+
)
|
|
133
|
+
except APIError as exc:
|
|
134
|
+
output.error(str(exc))
|
|
135
|
+
raise SystemExit(exc.exit_code)
|
|
136
|
+
|
|
137
|
+
note = Note.model_validate(data.get("note", {}))
|
|
138
|
+
output.success(f"Note #{note.id} created.")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@app.command("edit")
|
|
142
|
+
def note_edit(
|
|
143
|
+
note_id: int = typer.Argument(...),
|
|
144
|
+
title: Optional[str] = typer.Option(None, "--title", "-t"),
|
|
145
|
+
content: Optional[str] = typer.Option(None, "--content"),
|
|
146
|
+
editor: bool = typer.Option(False, "--editor", "-e"),
|
|
147
|
+
project_arg: Optional[str] = typer.Option(None, "--project", "-p"),
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Edit a note."""
|
|
150
|
+
client = _require_client()
|
|
151
|
+
project, session = guard_project_session(client, project_arg=project_arg)
|
|
152
|
+
sc = _session_client(client, session.id)
|
|
153
|
+
|
|
154
|
+
payload: dict = {}
|
|
155
|
+
if title is not None:
|
|
156
|
+
payload["title"] = title
|
|
157
|
+
|
|
158
|
+
if editor:
|
|
159
|
+
# Fetch current content first
|
|
160
|
+
try:
|
|
161
|
+
current_data = client.get(f"/api/cli/notes/{note_id}/")
|
|
162
|
+
current_note = current_data.get("note", {})
|
|
163
|
+
current_content = current_note.get("content", "")
|
|
164
|
+
except APIError:
|
|
165
|
+
current_content = ""
|
|
166
|
+
|
|
167
|
+
from projectmemory_cli.commands.memory import _open_in_editor
|
|
168
|
+
content = _open_in_editor(current_content)
|
|
169
|
+
|
|
170
|
+
if content is not None:
|
|
171
|
+
payload["content"] = content
|
|
172
|
+
|
|
173
|
+
if not payload:
|
|
174
|
+
output.warn("No changes specified.")
|
|
175
|
+
raise SystemExit(0)
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
sc.patch(f"/api/cli/notes/{note_id}/", json=payload, project_name=project.name)
|
|
179
|
+
except APIError as exc:
|
|
180
|
+
output.error(str(exc))
|
|
181
|
+
raise SystemExit(exc.exit_code)
|
|
182
|
+
output.success(f"Note #{note_id} updated.")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@app.command("delete")
|
|
186
|
+
def note_delete(
|
|
187
|
+
note_id: int = typer.Argument(...),
|
|
188
|
+
yes: bool = typer.Option(False, "--yes", "-y"),
|
|
189
|
+
project_arg: Optional[str] = typer.Option(None, "--project", "-p"),
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Delete a note."""
|
|
192
|
+
client = _require_client()
|
|
193
|
+
project, session = guard_project_session(client, project_arg=project_arg)
|
|
194
|
+
sc = _session_client(client, session.id)
|
|
195
|
+
|
|
196
|
+
if not yes:
|
|
197
|
+
confirmed = typer.confirm(f"Delete note #{note_id}?", default=False)
|
|
198
|
+
if not confirmed:
|
|
199
|
+
output.info("Cancelled.")
|
|
200
|
+
raise SystemExit(7)
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
sc.delete(f"/api/cli/notes/{note_id}/", project_name=project.name)
|
|
204
|
+
except APIError as exc:
|
|
205
|
+
output.error(str(exc))
|
|
206
|
+
raise SystemExit(exc.exit_code)
|
|
207
|
+
output.success(f"Note #{note_id} deleted.")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
# pm capture — quick note shortcut
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
@app.command("search")
|
|
215
|
+
def note_search(
|
|
216
|
+
query: str = typer.Argument(..., help="Search query."),
|
|
217
|
+
json: bool = typer.Option(False, "--json"),
|
|
218
|
+
project_arg: Optional[str] = typer.Option(None, "--project", "-p"),
|
|
219
|
+
) -> None:
|
|
220
|
+
"""Search notes in the current project."""
|
|
221
|
+
note_list(search=query, json=json, project_arg=project_arg)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def capture(
|
|
225
|
+
text: Optional[str] = typer.Argument(None, help="Note content."),
|
|
226
|
+
title: Optional[str] = typer.Option(None, "--title", "-t"),
|
|
227
|
+
stdin: bool = typer.Option(False, "--stdin"),
|
|
228
|
+
project_arg: Optional[str] = typer.Option(None, "--project", "-p"),
|
|
229
|
+
) -> None:
|
|
230
|
+
"""Quick note capture (shortcut for pm note add)."""
|
|
231
|
+
note_add(content=text, title=title, editor=False, stdin=stdin, project_arg=project_arg)
|