intent-cli-python 1.0.0__py3-none-any.whl → 1.2.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.
intent_cli/__init__.py CHANGED
@@ -1 +1,8 @@
1
1
  """Intent CLI — semantic history for agent-driven development."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("intent-cli-python")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0"
intent_cli/cli.py CHANGED
@@ -1,389 +1,60 @@
1
- """Intent CLI — entry point and command handlers."""
1
+ """Intent CLI — parser and command dispatch."""
2
2
 
3
3
  import argparse
4
- import json
5
4
  import sys
6
- from datetime import datetime, timezone
7
5
 
8
- from intent_cli.output import success, error
9
- from intent_cli.store import (
10
- git_root, ensure_init, init_workspace,
11
- next_id, read_object, write_object, list_objects, read_config,
6
+ from intent_cli.commands.core import (
7
+ cmd_decision_attach,
8
+ cmd_decision_create,
9
+ cmd_decision_deprecate,
10
+ cmd_decision_list,
11
+ cmd_decision_show,
12
+ cmd_doctor,
13
+ cmd_init,
14
+ cmd_inspect,
15
+ cmd_intent_activate,
16
+ cmd_intent_create,
17
+ cmd_intent_done,
18
+ cmd_intent_list,
19
+ cmd_intent_show,
20
+ cmd_intent_suspend,
21
+ cmd_snap_create,
22
+ cmd_snap_feedback,
23
+ cmd_snap_list,
24
+ cmd_snap_revert,
25
+ cmd_snap_show,
26
+ cmd_version,
12
27
  )
13
-
14
- VERSION = "1.0.0"
15
-
16
-
17
- def _now():
18
- return datetime.now(timezone.utc).isoformat()
19
-
20
-
21
- def _require_init():
22
- """Return .intent/ base path, or exit with error."""
23
- base = ensure_init()
24
- if base is not None:
25
- return base
26
- if git_root() is None:
27
- error("GIT_STATE_INVALID", "Not inside a Git repository.",
28
- suggested_fix="cd into a git repo and run: itt init")
29
- error("NOT_INITIALIZED", ".intent/ directory not found.",
30
- suggested_fix="itt init")
31
-
32
-
33
- # ---------------------------------------------------------------------------
34
- # Global commands
35
- # ---------------------------------------------------------------------------
36
-
37
- def cmd_version(_args):
38
- success("version", {"version": VERSION})
39
-
40
-
41
- def cmd_init(_args):
42
- path, err = init_workspace()
43
- if err == "GIT_STATE_INVALID":
44
- error("GIT_STATE_INVALID", "Not inside a Git repository.",
45
- suggested_fix="cd into a git repo and run: itt init")
46
- if err == "ALREADY_EXISTS":
47
- error("ALREADY_EXISTS", ".intent/ already exists.",
48
- suggested_fix="Remove .intent/ first if you want to reinitialize.")
49
- success("init", {"path": str(path)})
50
-
51
-
52
- def cmd_inspect(_args):
53
- base = _require_init()
54
- config = read_config(base)
55
-
56
- active_intents = []
57
- suspend_intents = []
58
- for obj in list_objects(base, "intent"):
59
- entry = {
60
- "id": obj["id"],
61
- "title": obj["title"],
62
- "status": obj["status"],
63
- "decision_ids": obj.get("decision_ids", []),
64
- "latest_snap_id": obj["snap_ids"][-1] if obj.get("snap_ids") else None,
65
- }
66
- if obj["status"] == "active":
67
- active_intents.append(entry)
68
- elif obj["status"] == "suspend":
69
- suspend_intents.append(entry)
70
-
71
- active_decisions = []
72
- for obj in list_objects(base, "decision", status="active"):
73
- active_decisions.append({
74
- "id": obj["id"],
75
- "title": obj["title"],
76
- "status": obj["status"],
77
- "intent_ids": obj.get("intent_ids", []),
78
- })
79
-
80
- all_snaps = list_objects(base, "snap")
81
- all_snaps.sort(key=lambda s: s.get("created_at", ""), reverse=True)
82
- recent_snaps = []
83
- for s in all_snaps[:10]:
84
- recent_snaps.append({
85
- "id": s["id"],
86
- "title": s["title"],
87
- "intent_id": s["intent_id"],
88
- "status": s["status"],
89
- "summary": s.get("summary", ""),
90
- "feedback": s.get("feedback", ""),
91
- })
92
-
93
- warnings = []
94
- intent_ids_on_disk = {o["id"] for o in list_objects(base, "intent")}
95
- for s in all_snaps:
96
- if s.get("intent_id") and s["intent_id"] not in intent_ids_on_disk:
97
- warnings.append(f"Orphan snap {s['id']}: intent {s['intent_id']} not found")
98
-
99
- print(json.dumps({
100
- "ok": True,
101
- "schema_version": config.get("schema_version", "1.0"),
102
- "active_intents": active_intents,
103
- "suspend_intents": suspend_intents,
104
- "active_decisions": active_decisions,
105
- "recent_snaps": recent_snaps,
106
- "warnings": warnings,
107
- }, indent=2, ensure_ascii=False))
108
-
109
-
110
- # ---------------------------------------------------------------------------
111
- # Intent commands
112
- # ---------------------------------------------------------------------------
113
-
114
- def cmd_intent_create(args):
115
- base = _require_init()
116
- obj_id = next_id(base, "intent")
117
-
118
- active_decisions = list_objects(base, "decision", status="active")
119
- decision_ids = [d["id"] for d in active_decisions]
120
-
121
- warnings = []
122
- if not decision_ids:
123
- warnings.append("No active decisions to attach.")
124
-
125
- intent = {
126
- "id": obj_id,
127
- "object": "intent",
128
- "created_at": _now(),
129
- "title": args.title,
130
- "status": "active",
131
- "source_query": args.query,
132
- "rationale": args.rationale,
133
- "decision_ids": decision_ids,
134
- "snap_ids": [],
135
- }
136
- write_object(base, "intent", obj_id, intent)
137
-
138
- for d in active_decisions:
139
- if obj_id not in d.get("intent_ids", []):
140
- d.setdefault("intent_ids", []).append(obj_id)
141
- write_object(base, "decision", d["id"], d)
142
-
143
- success("intent.create", intent, warnings)
144
-
145
-
146
- def cmd_intent_list(args):
147
- base = _require_init()
148
- success("intent.list", list_objects(base, "intent", status=args.status))
149
-
150
-
151
- def cmd_intent_show(args):
152
- base = _require_init()
153
- obj = read_object(base, "intent", args.id)
154
- if obj is None:
155
- error("OBJECT_NOT_FOUND", f"Intent {args.id} not found.")
156
- success("intent.show", obj)
157
-
158
-
159
- def cmd_intent_activate(args):
160
- base = _require_init()
161
- obj = read_object(base, "intent", args.id)
162
- if obj is None:
163
- error("OBJECT_NOT_FOUND", f"Intent {args.id} not found.")
164
- if obj["status"] != "suspend":
165
- error("STATE_CONFLICT",
166
- f"Cannot activate intent with status '{obj['status']}'. Only 'suspend' intents can be activated.",
167
- suggested_fix=f"itt intent show {args.id}")
168
-
169
- obj["status"] = "active"
170
-
171
- active_decisions = list_objects(base, "decision", status="active")
172
- for d in active_decisions:
173
- if d["id"] not in obj["decision_ids"]:
174
- obj["decision_ids"].append(d["id"])
175
- if args.id not in d.get("intent_ids", []):
176
- d.setdefault("intent_ids", []).append(args.id)
177
- write_object(base, "decision", d["id"], d)
178
-
179
- write_object(base, "intent", args.id, obj)
180
- success("intent.activate", obj)
181
-
182
-
183
- def cmd_intent_suspend(args):
184
- base = _require_init()
185
- obj = read_object(base, "intent", args.id)
186
- if obj is None:
187
- error("OBJECT_NOT_FOUND", f"Intent {args.id} not found.")
188
- if obj["status"] != "active":
189
- error("STATE_CONFLICT",
190
- f"Cannot suspend intent with status '{obj['status']}'. Only 'active' intents can be suspended.",
191
- suggested_fix=f"itt intent show {args.id}")
192
-
193
- obj["status"] = "suspend"
194
- write_object(base, "intent", args.id, obj)
195
- success("intent.suspend", obj)
196
-
197
-
198
- def cmd_intent_done(args):
199
- base = _require_init()
200
- obj = read_object(base, "intent", args.id)
201
- if obj is None:
202
- error("OBJECT_NOT_FOUND", f"Intent {args.id} not found.")
203
- if obj["status"] != "active":
204
- error("STATE_CONFLICT",
205
- f"Cannot mark intent as done with status '{obj['status']}'. Only 'active' intents can be marked done.",
206
- suggested_fix=f"itt intent show {args.id}")
207
-
208
- obj["status"] = "done"
209
- write_object(base, "intent", args.id, obj)
210
- success("intent.done", obj)
211
-
212
-
213
- # ---------------------------------------------------------------------------
214
- # Snap commands
215
- # ---------------------------------------------------------------------------
216
-
217
- def cmd_snap_create(args):
218
- base = _require_init()
219
- intent_id = args.intent
220
-
221
- intent = read_object(base, "intent", intent_id)
222
- if intent is None:
223
- error("OBJECT_NOT_FOUND", f"Intent {intent_id} not found.")
224
- if intent["status"] != "active":
225
- error("STATE_CONFLICT",
226
- f"Cannot add snap to intent with status '{intent['status']}'. Only 'active' intents accept new snaps.",
227
- suggested_fix=f"itt intent activate {intent_id}")
228
-
229
- obj_id = next_id(base, "snap")
230
- snap = {
231
- "id": obj_id,
232
- "object": "snap",
233
- "created_at": _now(),
234
- "title": args.title,
235
- "status": "active",
236
- "intent_id": intent_id,
237
- "query": args.query,
238
- "rationale": args.rationale,
239
- "summary": args.summary,
240
- "feedback": args.feedback,
241
- }
242
- write_object(base, "snap", obj_id, snap)
243
-
244
- intent.setdefault("snap_ids", []).append(obj_id)
245
- write_object(base, "intent", intent_id, intent)
246
-
247
- success("snap.create", snap)
248
-
249
-
250
- def cmd_snap_list(args):
251
- base = _require_init()
252
- objects = list_objects(base, "snap", status=args.status)
253
- if args.intent:
254
- objects = [s for s in objects if s.get("intent_id") == args.intent]
255
- success("snap.list", objects)
256
-
257
-
258
- def cmd_snap_show(args):
259
- base = _require_init()
260
- obj = read_object(base, "snap", args.id)
261
- if obj is None:
262
- error("OBJECT_NOT_FOUND", f"Snap {args.id} not found.")
263
- success("snap.show", obj)
264
-
265
-
266
- def cmd_snap_feedback(args):
267
- base = _require_init()
268
- obj = read_object(base, "snap", args.id)
269
- if obj is None:
270
- error("OBJECT_NOT_FOUND", f"Snap {args.id} not found.")
271
- obj["feedback"] = args.feedback
272
- write_object(base, "snap", args.id, obj)
273
- success("snap.feedback", obj)
274
-
275
-
276
- def cmd_snap_revert(args):
277
- base = _require_init()
278
- obj = read_object(base, "snap", args.id)
279
- if obj is None:
280
- error("OBJECT_NOT_FOUND", f"Snap {args.id} not found.")
281
- if obj["status"] != "active":
282
- error("STATE_CONFLICT",
283
- f"Cannot revert snap with status '{obj['status']}'. Only 'active' snaps can be reverted.",
284
- suggested_fix=f"itt snap show {args.id}")
285
-
286
- obj["status"] = "reverted"
287
- write_object(base, "snap", args.id, obj)
288
- success("snap.revert", obj)
289
-
290
-
291
- # ---------------------------------------------------------------------------
292
- # Decision commands
293
- # ---------------------------------------------------------------------------
294
-
295
- def cmd_decision_create(args):
296
- base = _require_init()
297
- obj_id = next_id(base, "decision")
298
-
299
- active_intents = list_objects(base, "intent", status="active")
300
- intent_ids = [i["id"] for i in active_intents]
301
-
302
- warnings = []
303
- if not intent_ids:
304
- warnings.append("No active intents to attach.")
305
-
306
- decision = {
307
- "id": obj_id,
308
- "object": "decision",
309
- "created_at": _now(),
310
- "title": args.title,
311
- "status": "active",
312
- "rationale": args.rationale,
313
- "intent_ids": intent_ids,
314
- }
315
- write_object(base, "decision", obj_id, decision)
316
-
317
- for i in active_intents:
318
- if obj_id not in i.get("decision_ids", []):
319
- i.setdefault("decision_ids", []).append(obj_id)
320
- write_object(base, "intent", i["id"], i)
321
-
322
- success("decision.create", decision, warnings)
323
-
324
-
325
- def cmd_decision_list(args):
326
- base = _require_init()
327
- success("decision.list", list_objects(base, "decision", status=args.status))
328
-
329
-
330
- def cmd_decision_show(args):
331
- base = _require_init()
332
- obj = read_object(base, "decision", args.id)
333
- if obj is None:
334
- error("OBJECT_NOT_FOUND", f"Decision {args.id} not found.")
335
- success("decision.show", obj)
336
-
337
-
338
- def cmd_decision_deprecate(args):
339
- base = _require_init()
340
- obj = read_object(base, "decision", args.id)
341
- if obj is None:
342
- error("OBJECT_NOT_FOUND", f"Decision {args.id} not found.")
343
- if obj["status"] != "active":
344
- error("STATE_CONFLICT",
345
- f"Cannot deprecate decision with status '{obj['status']}'. Only 'active' decisions can be deprecated.",
346
- suggested_fix=f"itt decision show {args.id}")
347
-
348
- obj["status"] = "deprecated"
349
- write_object(base, "decision", args.id, obj)
350
- success("decision.deprecate", obj)
28
+ from intent_cli.commands.hub import cmd_hub_link, cmd_hub_login, cmd_hub_sync
351
29
 
352
30
 
353
- def cmd_decision_attach(args):
354
- base = _require_init()
355
- decision = read_object(base, "decision", args.id)
356
- if decision is None:
357
- error("OBJECT_NOT_FOUND", f"Decision {args.id} not found.")
358
-
359
- intent_id = args.intent
360
- intent = read_object(base, "intent", intent_id)
361
- if intent is None:
362
- error("OBJECT_NOT_FOUND", f"Intent {intent_id} not found.")
363
-
364
- if intent_id not in decision.get("intent_ids", []):
365
- decision.setdefault("intent_ids", []).append(intent_id)
366
- write_object(base, "decision", args.id, decision)
367
-
368
- if args.id not in intent.get("decision_ids", []):
369
- intent.setdefault("decision_ids", []).append(args.id)
370
- write_object(base, "intent", intent_id, intent)
371
-
372
- success("decision.attach", decision)
373
-
374
-
375
- # ---------------------------------------------------------------------------
376
- # Argument parser
377
- # ---------------------------------------------------------------------------
378
-
379
31
  def main():
380
32
  parser = argparse.ArgumentParser(prog="itt", description="Intent CLI")
381
33
  sub = parser.add_subparsers(dest="command")
382
34
 
383
- # version / init / inspect
35
+ # version / init / inspect / doctor
384
36
  sub.add_parser("version")
385
37
  sub.add_parser("init")
386
38
  sub.add_parser("inspect")
39
+ sub.add_parser("doctor")
40
+
41
+ # --- hub ---
42
+ p_hub = sub.add_parser("hub")
43
+ s_hub = p_hub.add_subparsers(dest="sub")
44
+
45
+ p = s_hub.add_parser("login")
46
+ p.add_argument("--api-base-url", default=None)
47
+ p.add_argument("--token", default=None)
48
+
49
+ p = s_hub.add_parser("link")
50
+ p.add_argument("--project-name", default=None)
51
+ p.add_argument("--api-base-url", default=None)
52
+ p.add_argument("--token", default=None)
53
+
54
+ p = s_hub.add_parser("sync")
55
+ p.add_argument("--api-base-url", default=None)
56
+ p.add_argument("--token", default=None)
57
+ p.add_argument("--dry-run", action="store_true")
387
58
 
388
59
  # --- intent ---
389
60
  p_intent = sub.add_parser("intent")
@@ -396,6 +67,7 @@ def main():
396
67
 
397
68
  p = s_intent.add_parser("list")
398
69
  p.add_argument("--status", default=None)
70
+ p.add_argument("--decision", default=None)
399
71
 
400
72
  p = s_intent.add_parser("show")
401
73
  p.add_argument("id")
@@ -445,6 +117,7 @@ def main():
445
117
 
446
118
  p = s_decision.add_parser("list")
447
119
  p.add_argument("--status", default=None)
120
+ p.add_argument("--intent", default=None)
448
121
 
449
122
  p = s_decision.add_parser("show")
450
123
  p.add_argument("id")
@@ -456,7 +129,6 @@ def main():
456
129
  p.add_argument("id")
457
130
  p.add_argument("--intent", required=True)
458
131
 
459
- # --- dispatch ---
460
132
  args = parser.parse_args()
461
133
 
462
134
  if args.command is None:
@@ -467,31 +139,40 @@ def main():
467
139
  "version": cmd_version,
468
140
  "init": cmd_init,
469
141
  "inspect": cmd_inspect,
142
+ "doctor": cmd_doctor,
470
143
  }
471
144
  if args.command in dispatch_global:
472
145
  dispatch_global[args.command](args)
473
146
  return
474
147
 
475
148
  if not getattr(args, "sub", None):
476
- {"intent": p_intent, "snap": p_snap, "decision": p_decision}[args.command].print_help()
149
+ {
150
+ "hub": p_hub,
151
+ "intent": p_intent,
152
+ "snap": p_snap,
153
+ "decision": p_decision,
154
+ }[args.command].print_help()
477
155
  sys.exit(1)
478
156
 
479
157
  dispatch = {
480
- ("intent", "create"): cmd_intent_create,
481
- ("intent", "list"): cmd_intent_list,
482
- ("intent", "show"): cmd_intent_show,
483
- ("intent", "activate"): cmd_intent_activate,
484
- ("intent", "suspend"): cmd_intent_suspend,
485
- ("intent", "done"): cmd_intent_done,
486
- ("snap", "create"): cmd_snap_create,
487
- ("snap", "list"): cmd_snap_list,
488
- ("snap", "show"): cmd_snap_show,
489
- ("snap", "feedback"): cmd_snap_feedback,
490
- ("snap", "revert"): cmd_snap_revert,
491
- ("decision", "create"): cmd_decision_create,
492
- ("decision", "list"): cmd_decision_list,
493
- ("decision", "show"): cmd_decision_show,
494
- ("decision", "deprecate"): cmd_decision_deprecate,
495
- ("decision", "attach"): cmd_decision_attach,
158
+ ("hub", "login"): cmd_hub_login,
159
+ ("hub", "link"): cmd_hub_link,
160
+ ("hub", "sync"): cmd_hub_sync,
161
+ ("intent", "create"): cmd_intent_create,
162
+ ("intent", "list"): cmd_intent_list,
163
+ ("intent", "show"): cmd_intent_show,
164
+ ("intent", "activate"): cmd_intent_activate,
165
+ ("intent", "suspend"): cmd_intent_suspend,
166
+ ("intent", "done"): cmd_intent_done,
167
+ ("snap", "create"): cmd_snap_create,
168
+ ("snap", "list"): cmd_snap_list,
169
+ ("snap", "show"): cmd_snap_show,
170
+ ("snap", "feedback"): cmd_snap_feedback,
171
+ ("snap", "revert"): cmd_snap_revert,
172
+ ("decision", "create"): cmd_decision_create,
173
+ ("decision", "list"): cmd_decision_list,
174
+ ("decision", "show"): cmd_decision_show,
175
+ ("decision", "deprecate"): cmd_decision_deprecate,
176
+ ("decision", "attach"): cmd_decision_attach,
496
177
  }
497
178
  dispatch[(args.command, args.sub)](args)
@@ -0,0 +1 @@
1
+ """Command handlers for the Intent CLI."""
@@ -0,0 +1,41 @@
1
+ """Shared helpers for CLI command handlers."""
2
+
3
+ from datetime import datetime, timezone
4
+
5
+ from intent_cli.output import error
6
+ from intent_cli.store import VALID_STATUSES, ensure_init, git_root
7
+
8
+
9
+ def now_utc():
10
+ return datetime.now(timezone.utc).isoformat()
11
+
12
+
13
+ def require_init():
14
+ """Return .intent/ base path, or exit with a structured error."""
15
+ base = ensure_init()
16
+ if base is not None:
17
+ return base
18
+ if git_root() is None:
19
+ error(
20
+ "GIT_STATE_INVALID",
21
+ "Not inside a Git repository.",
22
+ suggested_fix="cd into a git repo and run: itt init",
23
+ )
24
+ error(
25
+ "NOT_INITIALIZED",
26
+ ".intent/ directory not found.",
27
+ suggested_fix="itt init",
28
+ )
29
+
30
+
31
+ def validate_status_filter(object_type, status):
32
+ """Validate a --status filter against the object's state machine."""
33
+ if status is None:
34
+ return
35
+ allowed = sorted(VALID_STATUSES[object_type])
36
+ if status not in allowed:
37
+ error(
38
+ "INVALID_INPUT",
39
+ f"Invalid status '{status}' for {object_type}. Allowed values: {', '.join(allowed)}.",
40
+ suggested_fix=f"Use one of: {', '.join(allowed)}",
41
+ )