diff --git a/README.md b/README.md
index 93f3df4..65a8c70 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@ The [`Dialog`](https://AnswerDotAI.github.io/aidialog/dialog.html#dialog) is the
| canonical chat ([`Msg`](https://AnswerDotAI.github.io/aidialog/msg_parts.html#msg)/[`Part`](https://AnswerDotAI.github.io/aidialog/msg_parts.html#part)) | transmission, normalizing | [`chat2dlg`](https://AnswerDotAI.github.io/aidialog/hist.html#chat2dlg) | [`dlg2chat`](https://AnswerDotAI.github.io/aidialog/hist.html#dlg2chat) |
| hist (live call input) | transmission, one-way | | [`dlg2hist`](https://AnswerDotAI.github.io/aidialog/hist.html#dlg2hist) |
| a prompt’s reply | self-similar | [`reply2dlg`](https://AnswerDotAI.github.io/aidialog/hist.html#reply2dlg) | [`dlg2reply`](https://AnswerDotAI.github.io/aidialog/hist.html#dlg2reply) |
-| XML views | display, one-way | | [`view_dlg`](https://AnswerDotAI.github.io/aidialog/dlgskill.html#view_dlg), [`msg2xml`](https://AnswerDotAI.github.io/aidialog/dlgskill.html#msg2xml) |
+| XML views | display, one-way | | [`view_dlg`](https://AnswerDotAI.github.io/aidialog/dlgskill.html#view_dlg), [`msg2xml`](https://AnswerDotAI.github.io/aidialog/dialog.html#msg2xml) |
The session codecs (in [llmsurgery](https://github.com/AnswerDotAI/llmsurgery)) route through chat on their way to the wire: ant’s `dlg2msgs` and oai’s `dlg2items` are each `denorm_msgs(dlg2chat(...))`.
@@ -67,11 +67,28 @@ A quick taste - create a dialog, add a message, and view it as concise XML:
``` python
from aidialog.dlgskill import *
import tempfile
+```
+``` python
p = tempfile.mkdtemp() + '/demo.ipynb'
-create_dlg(p, '## A tiny dialog', 'note')
-add_msg('6*7', after=find_msgs(dlg=p)[0].id, dlg=p)
+d = create_dlg(p, '## A tiny dialog', 'note')
+add_msg('6*7', after=d.messages[0].id, dlg=p)
view_dlg(p)
```
-
+
+
+## Command line
+
+The flat commands expose dialog-aware reading and structural edits without a Python kernel:
+
+``` bash
+aidialog-summary nbs/00_core.ipynb
+aidialog-find nbs/00_core.ipynb 'read_csv' --context 1
+aidialog-view nbs/00_core.ipynb ab12cd34 --out
+aidialog-add nbs/00_core.ipynb --after ab12cd34 --msg-type code < new-cell.py
+aidialog-del nbs/00_core.ipynb ab12cd34
+aidialog-move nbs/00_core.ipynb ab12cd34,ef56ab78 --before 9012cdef
+```
+
+Message ID arguments are comma-separated where a command accepts several. Mutating commands accept `--dry-run`; run any command with `--help` for its full filters and display options.
diff --git a/aidialog/_modidx.py b/aidialog/_modidx.py
index 985c61b..ec907bf 100644
--- a/aidialog/_modidx.py
+++ b/aidialog/_modidx.py
@@ -5,7 +5,8 @@
'doc_host': 'https://AnswerDotAI.github.io',
'git_url': 'https://github.com/AnswerDotAI/aidialog',
'lib_path': 'aidialog'},
- 'syms': { 'aidialog.dialog': { 'aidialog.dialog.Attachment': ('dialog.html#attachment', 'aidialog/dialog.py'),
+ 'syms': { 'aidialog.cli': {},
+ 'aidialog.dialog': { 'aidialog.dialog.Attachment': ('dialog.html#attachment', 'aidialog/dialog.py'),
'aidialog.dialog.Attachment.__init__': ('dialog.html#attachment.__init__', 'aidialog/dialog.py'),
'aidialog.dialog.Dialog': ('dialog.html#dialog', 'aidialog/dialog.py'),
'aidialog.dialog.Dialog.__bool__': ('dialog.html#dialog.__bool__', 'aidialog/dialog.py'),
diff --git a/aidialog/cli.py b/aidialog/cli.py
new file mode 100644
index 0000000..ec769ff
--- /dev/null
+++ b/aidialog/cli.py
@@ -0,0 +1,125 @@
+"Command-line access to dialog and notebook navigation and structure."
+
+import sys
+from fastcore.script import call_parse
+from .dlgskill import summary_dlg, find_msgs, view_dlg, view_msgs
+from .ipynb import read_ipynb
+
+
+def _die(msg): raise SystemExit(msg)
+
+
+def _dialog(fname):
+ if (res := read_ipynb(fname)) is None: _die(f"Could not read dialog: {fname}")
+ return res
+
+
+def _placement(before, after, required=False):
+ if before and after: _die("Pass only one of --before and --after")
+ if required and not (before or after): _die("Pass one of --before and --after")
+
+
+def _ids(ids): return [o.strip() for o in ids.split(',') if o.strip()]
+
+
+@call_parse
+def summary_cli(
+ fname:str, # Dialog or notebook path
+ maxlen:int=180, # Maximum characters displayed per message
+):
+ "Show one preview row per message."
+ print(summary_dlg(fname, maxlen=maxlen))
+
+
+@call_parse(pos=['pattern'])
+def find_cli(
+ fname:str, # Dialog or notebook path
+ pattern:str='', # Regex or plain text to find
+ msg_type:str=None, # Limit matches to code, note, prompt, or raw messages
+ errors:bool=False, # Match only code messages with errors?
+ exported:bool=False, # Match only exported messages?
+ ids:str='', # Comma-separated message IDs to select
+ before:int=0, # Messages of context before each match
+ after:int=0, # Messages of context after each match
+ context:int=None, # Messages of context before and after each match
+ limit:int=None, # Maximum matched messages
+ case:bool=False, # Match case sensitively?
+ plain:bool=False, # Treat pattern as plain text rather than regex?
+ headers:bool=False, # Match only heading notes?
+ section:str=None, # Return the section beginning at this heading
+):
+ "Find messages using dialog-aware filters and context."
+ print(find_msgs(pattern, dlg=fname, msg_type=msg_type, only_err=errors, only_exp=exported, ids=ids,
+ before=before, after=after, context=context, limit=limit, use_case=case, use_regex=not plain,
+ headers_only=headers, header_section=section))
+
+
+@call_parse(pos=['ids'])
+def view_cli(
+ fname:str, # Dialog or notebook path
+ ids:str='', # Comma-separated message IDs; omit to view the whole dialog
+ nums:bool=True, # Show line numbers for individual messages?
+ lnhashs:bool=False, # Show hash-verified line addresses instead of line numbers?
+ start_line:int=1, # First source line to show
+ end_line:int=None, # Last source line to show
+ out:bool=False, # Include prompt replies and code outputs?
+ full_out:bool=False, # Do not truncate included outputs?
+ errors:bool=False, # Show only code messages with errors?
+):
+ "View complete dialogs or selected messages."
+ if (sel := _ids(ids)):
+ if errors: _die("--errors applies only when viewing a whole dialog")
+ res = view_msgs(*sel, dlg=fname, nums=nums, start_line=start_line, end_line=end_line,
+ lnhashs=lnhashs, incl_out=out, trunc_out=not full_out)
+ else:
+ if lnhashs or not nums or start_line != 1 or end_line is not None: _die("Line options require message IDs")
+ res = view_dlg(fname, incl_out=out, only_errors=errors, trunc_out=not full_out)
+ print(res)
+
+
+@call_parse
+def add_cli(
+ fname:str, # Dialog or notebook path
+ source:str=None, # Message source; defaults to stdin
+ msg_type:str='code', # Message type: code, note, prompt, or raw
+ before:str=None, # Insert before this message ID
+ after:str=None, # Insert after this message ID
+ export:bool=False, # Mark the new message for nbdev export?
+ dry_run:bool=False, # Preview without saving?
+):
+ "Add a message, reading multiline source from stdin by default."
+ _placement(before, after)
+ if source is None: source = sys.stdin.read()
+ d = _dialog(fname)
+ m = d.mk_message(source, before=before, after=after, msg_type=msg_type, export=export)
+ if not dry_run: d.save()
+ print(m.preview())
+
+
+@call_parse
+def del_cli(
+ fname:str, # Dialog or notebook path
+ ids:str, # Comma-separated message IDs to delete
+ dry_run:bool=False, # Preview without saving?
+):
+ "Delete messages by stable ID."
+ d = _dialog(fname)
+ removed = d.remove_msgs([d.msg(i) for i in _ids(ids)])
+ if not dry_run: d.save()
+ print('\n'.join(str(m.preview()) for m in removed))
+
+
+@call_parse
+def move_cli(
+ fname:str, # Dialog or notebook path
+ ids:str, # Comma-separated message IDs to move
+ before:str=None, # Move before this message ID
+ after:str=None, # Move after this message ID
+ dry_run:bool=False, # Preview without saving?
+):
+ "Move messages while retaining their relative order."
+ _placement(before, after, required=True)
+ d = _dialog(fname)
+ d.move_msgs(_ids(ids), before=before, after=after)
+ if not dry_run: d.save()
+ print(d.summary())
diff --git a/nbs/index.ipynb b/nbs/index.ipynb
index ef813ad..2d650ba 100644
--- a/nbs/index.ipynb
+++ b/nbs/index.ipynb
@@ -1,16 +1,5 @@
{
"cells": [
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "f7806d71",
- "metadata": {},
- "outputs": [],
- "source": [
- "#| hide\n",
- "from aidialog.dlgskill import *"
- ]
- },
{
"cell_type": "markdown",
"id": "7b8db7d9",
@@ -162,6 +151,17 @@
"A quick taste - create a dialog, add a message, and view it as concise XML:"
]
},
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f7806d71",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from aidialog.dlgskill import *\n",
+ "import tempfile"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -180,14 +180,32 @@
}
],
"source": [
- "from aidialog.dlgskill import *\n",
- "import tempfile\n",
- "\n",
"p = tempfile.mkdtemp() + '/demo.ipynb'\n",
"d = create_dlg(p, '## A tiny dialog', 'note')\n",
"add_msg('6*7', after=d.messages[0].id, dlg=p)\n",
"view_dlg(p)"
]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "dfad191c",
+ "metadata": {},
+ "source": [
+ "## Command line\n",
+ "\n",
+ "The flat commands expose dialog-aware reading and structural edits without a Python kernel:\n",
+ "\n",
+ "```bash\n",
+ "aidialog-summary nbs/00_core.ipynb\n",
+ "aidialog-find nbs/00_core.ipynb 'read_csv' --context 1\n",
+ "aidialog-view nbs/00_core.ipynb ab12cd34 --out\n",
+ "aidialog-add nbs/00_core.ipynb --after ab12cd34 --msg-type code < new-cell.py\n",
+ "aidialog-del nbs/00_core.ipynb ab12cd34\n",
+ "aidialog-move nbs/00_core.ipynb ab12cd34,ef56ab78 --before 9012cdef\n",
+ "```\n",
+ "\n",
+ "Message ID arguments are comma-separated where a command accepts several. Mutating commands accept `--dry-run`; run any command with `--help` for its full filters and display options."
+ ]
}
],
"metadata": {
diff --git a/pyproject.toml b/pyproject.toml
index d17af13..2f1cb2b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,6 +30,14 @@ aidialog = "aidialog._modidx:d"
[project.entry-points.pyskills]
"aidialog.dlgskill" = "aidialog.dlgskill"
+[project.scripts]
+aidialog-summary = "aidialog.cli:summary_cli"
+aidialog-find = "aidialog.cli:find_cli"
+aidialog-view = "aidialog.cli:view_cli"
+aidialog-add = "aidialog.cli:add_cli"
+aidialog-del = "aidialog.cli:del_cli"
+aidialog-move = "aidialog.cli:move_cli"
+
[tool.setuptools.dynamic]
version = {attr = "aidialog.__version__"}