@cteno/sdk-linux-x64 0.0.0-stage → 2.10.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/LICENSE.daemon +31 -0
  2. package/README.md +4 -2
  3. package/agents/flow-analyzer/AGENT.md +71 -0
  4. package/bin/cteno-host-cli +4 -0
  5. package/bin/cteno-wiki +0 -0
  6. package/bin/rg +0 -0
  7. package/bin/secret-exec +0 -0
  8. package/manifest.json +12 -0
  9. package/package.json +33 -4
  10. package/plugins/agent-secrets/.cteno-plugin/plugin.json +133 -0
  11. package/plugins/agent-secrets/bin/linux-x64/fnox +4 -0
  12. package/plugins/agent-secrets/bin/linux-x64/secret-exec +0 -0
  13. package/plugins/agent-secrets/skills/secret/SKILL.md +201 -0
  14. package/plugins/cteno.browser/.cteno-plugin/plugin.json +65 -0
  15. package/plugins/cteno.browser/skills/browser/SKILL.md +28 -0
  16. package/plugins/cteno.inbox/.cteno-plugin/plugin.json +128 -0
  17. package/plugins/cteno.inbox/bin/all/cteno-connector-credential +31 -0
  18. package/plugins/cteno.inbox/bin/all/cteno-telegram-connector +33 -0
  19. package/plugins/cteno.inbox/bin/linux-x64/cteno-inbox +0 -0
  20. package/plugins/cteno.inbox/skills/cteno-inbox/SKILL.md +374 -0
  21. package/plugins/cteno.inbox/tools/inbox_focus/TOOL.md +44 -0
  22. package/plugins/cteno.memory/.cteno-plugin/plugin.json +176 -0
  23. package/plugins/cteno.memory/agents/llm-wiki/AGENT.md +96 -0
  24. package/plugins/cteno.memory/bin/linux-x64/cteno-wiki +0 -0
  25. package/plugins/cteno.memory/skills/codebase-wiki/SKILL.md +52 -0
  26. package/plugins/cteno.memory/skills/cteno.memory.native/SKILL.md +110 -0
  27. package/plugins/cteno.memory/tools/memory/TOOL.md +492 -0
  28. package/plugins/cteno.memory/ui/index.html +822 -0
  29. package/plugins/cteno.models/.cteno-plugin/plugin.json +191 -0
  30. package/plugins/cteno.websearch/.cteno-plugin/plugin.json +45 -0
  31. package/plugins/jev/.cteno-plugin/plugin.json +305 -0
  32. package/plugins/jev/skills/jev-browser/SKILL.md +66 -0
  33. package/plugins/jev/skills/jev-browser/jev_browser.py +335 -0
  34. package/skills/agent-create/SKILL.md +86 -0
  35. package/skills/cteno-host-cli/SKILL.md +248 -0
  36. package/skills/docx/LICENSE +23 -0
  37. package/skills/docx/NOTICE.md +8 -0
  38. package/skills/docx/SKILL.md +69 -0
  39. package/skills/docx/references/cjk_typography.md +357 -0
  40. package/skills/docx/references/design_principles.md +819 -0
  41. package/skills/docx/references/revisions-and-comments.md +88 -0
  42. package/skills/docx/references/track_changes_guide.md +200 -0
  43. package/skills/docx/references/typography_guide.md +294 -0
  44. package/skills/docx/scripts/cjk_fonts.py +244 -0
  45. package/skills/docx/scripts/docx_comments.py +289 -0
  46. package/skills/docx/scripts/docx_common.py +92 -0
  47. package/skills/docx/scripts/docx_create.py +259 -0
  48. package/skills/docx/scripts/docx_edit.py +250 -0
  49. package/skills/docx/scripts/docx_read.py +149 -0
  50. package/skills/docx/scripts/docx_revisions.py +146 -0
  51. package/skills/docx/scripts/docx_template.py +70 -0
  52. package/skills/docx/scripts/docx_validate.py +156 -0
  53. package/skills/docx/scripts/render.py +250 -0
  54. package/skills/docx/tests/test_docx_skill.py +525 -0
  55. package/skills/home-devices/SKILL.md +121 -0
  56. package/skills/mcp/SKILL.md +72 -0
  57. package/skills/mcp/scripts/mcp_setup.py +439 -0
  58. package/skills/pdf/LICENSE +23 -0
  59. package/skills/pdf/NOTICE.md +8 -0
  60. package/skills/pdf/SKILL.md +63 -0
  61. package/skills/pdf/references/design-tokens.md +381 -0
  62. package/skills/pdf/references/forms.md +99 -0
  63. package/skills/pdf/references/nano-pdf-editing.md +40 -0
  64. package/skills/pdf/references/ocr-extraction.md +165 -0
  65. package/skills/pdf/scripts/_raster.py +76 -0
  66. package/skills/pdf/scripts/extract_marker.py +88 -0
  67. package/skills/pdf/scripts/extract_pymupdf.py +101 -0
  68. package/skills/pdf/scripts/pdf_create.py +191 -0
  69. package/skills/pdf/scripts/pdf_fill_form.py +97 -0
  70. package/skills/pdf/scripts/pdf_form_layout.py +168 -0
  71. package/skills/pdf/scripts/pdf_make_form.py +145 -0
  72. package/skills/pdf/scripts/pdf_merge.py +50 -0
  73. package/skills/pdf/scripts/pdf_meta.py +115 -0
  74. package/skills/pdf/scripts/pdf_page_image.py +99 -0
  75. package/skills/pdf/scripts/pdf_read.py +153 -0
  76. package/skills/pdf/scripts/pdf_secure.py +71 -0
  77. package/skills/pdf/scripts/pdf_split.py +84 -0
  78. package/skills/pdf/scripts/pdf_stamp.py +143 -0
  79. package/skills/pdf/scripts/pdf_watermark.py +51 -0
  80. package/skills/pdf/scripts/render.py +250 -0
  81. package/skills/pdf/tests/test_pdf_skill.py +415 -0
  82. package/skills/pptx/LICENSE +23 -0
  83. package/skills/pptx/NOTICE.md +8 -0
  84. package/skills/pptx/SKILL.md +62 -0
  85. package/skills/pptx/references/design-system.md +392 -0
  86. package/skills/pptx/references/pptxgenjs.md +420 -0
  87. package/skills/pptx/references/slide-types.md +413 -0
  88. package/skills/pptx/scripts/cjk_fonts.py +244 -0
  89. package/skills/pptx/scripts/pptx_create.py +240 -0
  90. package/skills/pptx/scripts/pptx_edit.py +436 -0
  91. package/skills/pptx/scripts/pptx_from_template.py +88 -0
  92. package/skills/pptx/scripts/pptx_read.py +131 -0
  93. package/skills/pptx/scripts/pptx_render.py +96 -0
  94. package/skills/pptx/scripts/render.py +250 -0
  95. package/skills/pptx/tests/test_powerpoint_skill.py +477 -0
  96. package/skills/skill-create/SKILL.md +114 -0
  97. package/skills/video-generation/SKILL.md +27 -0
  98. package/skills/xlsx/LICENSE +23 -0
  99. package/skills/xlsx/NOTICE.md +8 -0
  100. package/skills/xlsx/SKILL.md +63 -0
  101. package/skills/xlsx/references/format.md +71 -0
  102. package/skills/xlsx/references/restructuring.md +71 -0
  103. package/skills/xlsx/scripts/csv_to_xlsx.py +110 -0
  104. package/skills/xlsx/scripts/formula_check.py +428 -0
  105. package/skills/xlsx/scripts/render.py +250 -0
  106. package/skills/xlsx/scripts/xlsx_create.py +265 -0
  107. package/skills/xlsx/scripts/xlsx_edit.py +269 -0
  108. package/skills/xlsx/scripts/xlsx_read.py +166 -0
  109. package/skills/xlsx/scripts/xlsx_recalc.py +119 -0
  110. package/skills/xlsx/scripts/xlsx_restructure.py +343 -0
  111. package/skills/xlsx/scripts/xlsx_to_csv.py +75 -0
  112. package/skills/xlsx/tests/test_xlsx_skill.py +544 -0
  113. package/tools/create_goal/TOOL.md +36 -0
  114. package/tools/image_generation/TOOL.md +81 -0
  115. package/tools/skill/TOOL.md +117 -0
  116. package/tools/update_profile_notes/TOOL.md +39 -0
@@ -0,0 +1,477 @@
1
+ """End-to-end tests for the powerpoint skill helper scripts.
2
+
3
+ Runs entirely offline. Exercises create, read, template-fill (with
4
+ non-ASCII values), and edit (text + chart data + remove/move slide).
5
+ """
6
+ import json
7
+ import os
8
+ import subprocess
9
+ import sys
10
+
11
+ import pytest
12
+
13
+ SKILL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
14
+ SCRIPTS = os.path.join(SKILL, "scripts")
15
+
16
+
17
+ def run(script, *args):
18
+ env = dict(os.environ, LC_ALL="C", PYTHONIOENCODING="utf-8")
19
+ proc = subprocess.run(
20
+ [sys.executable, os.path.join(SCRIPTS, script), *args],
21
+ capture_output=True, text=True, encoding="utf-8", env=env, check=False)
22
+ assert proc.returncode == 0, f"{script} failed: {proc.stderr}"
23
+ return json.loads(proc.stdout)
24
+
25
+
26
+ def write_json(path, obj):
27
+ with open(path, "w", encoding="utf-8") as fh:
28
+ json.dump(obj, fh, ensure_ascii=False)
29
+
30
+
31
+ @pytest.fixture(scope="module")
32
+ def workdir(tmp_path_factory):
33
+ return tmp_path_factory.mktemp("pptx")
34
+
35
+
36
+ @pytest.fixture(scope="module")
37
+ def sample_png(workdir):
38
+ # 1x1 red PNG, hardcoded bytes -> no Pillow dependency.
39
+ import base64
40
+ data = base64.b64decode(
41
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4"
42
+ "z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==")
43
+ path = workdir / "red.png"
44
+ path.write_bytes(data)
45
+ return str(path)
46
+
47
+
48
+ @pytest.fixture(scope="module")
49
+ def deck(workdir, sample_png):
50
+ """Create a deck exercising every create feature."""
51
+ spec = {
52
+ "slide_size": "16:9",
53
+ "slides": [
54
+ {"layout": "title", "title": "Annual Review",
55
+ "subtitle": "Fiscal year 2026",
56
+ "notes": "Welcome the audience."},
57
+ {"layout": "title_content", "title": "Agenda",
58
+ "bullets": [
59
+ "Overview",
60
+ {"text": "Details", "level": 1, "bold": True,
61
+ "size": 20, "color": "CC0000", "font": "Arial"},
62
+ {"text": "Deep dive", "level": 2, "italic": True},
63
+ ],
64
+ "notes": "Keep this under two minutes."},
65
+ {"layout": "blank", "title": None,
66
+ "images": [{"path": sample_png, "left": 0.5, "top": 0.5,
67
+ "width": 2, "height": 2}],
68
+ "tables": [{"left": 3, "top": 1, "width": 6, "height": 2,
69
+ "rows": [["Region", "Sales"],
70
+ ["North", "120"], ["South", "80"]]}],
71
+ "shapes": [{"type": "rounded_rectangle", "left": 10,
72
+ "top": 1, "width": 2.5, "height": 1,
73
+ "fill": "4472C4", "text": "Callout",
74
+ "text_color": "FFFFFF"}]},
75
+ {"layout": "title_only", "title": "Charts",
76
+ "charts": [
77
+ {"type": "bar", "left": 0.5, "top": 1.5, "width": 4,
78
+ "height": 3, "title": "Sales",
79
+ "categories": ["Q1", "Q2"],
80
+ "series": {"North": [10, 20], "South": [7, 13]}},
81
+ {"type": "line", "left": 4.7, "top": 1.5, "width": 4,
82
+ "height": 3, "categories": ["Jan", "Feb", "Mar"],
83
+ "series": {"Trend": [1, 3, 2]}},
84
+ {"type": "pie", "left": 8.9, "top": 1.5, "width": 4,
85
+ "height": 3, "categories": ["A", "B", "C"],
86
+ "series": {"Share": [50, 30, 20]}}]},
87
+ ],
88
+ }
89
+ spec_path = workdir / "deck.json"
90
+ write_json(spec_path, spec)
91
+ out = workdir / "deck.pptx"
92
+ result = run("pptx_create.py", str(spec_path), str(out))
93
+ assert result["ok"] and result["slides"] == 4
94
+ return str(out)
95
+
96
+
97
+ def test_create_and_outline(deck):
98
+ outline = run("pptx_read.py", deck, "--outline")
99
+ assert outline["slide_count"] == 4
100
+ assert outline["slide_size_inches"][0] == pytest.approx(13.333, abs=0.01)
101
+ s0, s1, s2, s3 = outline["slides"]
102
+ assert "Annual Review" in s0["texts"]
103
+ assert "Fiscal year 2026" in s0["texts"]
104
+ assert s0["notes"] == "Welcome the audience."
105
+ # bullets present on slide 1
106
+ assert any("Deep dive" in t for t in s1["texts"])
107
+ # table content
108
+ assert s2["tables"][0][0] == ["Region", "Sales"]
109
+ assert s2["tables"][0][2] == ["South", "80"]
110
+ # image inventory + shape text
111
+ assert len(s2["images"]) == 1 and s2["images"][0]["ext"] == "png"
112
+ assert "Callout" in s2["texts"]
113
+ # three charts with correct data
114
+ assert len(s3["charts"]) == 3
115
+ bar = s3["charts"][0]
116
+ assert bar["categories"] == ["Q1", "Q2"]
117
+ north = next(s for s in bar["series"] if s["name"] == "North")
118
+ assert north["values"] == [10.0, 20.0]
119
+
120
+
121
+ def test_create_43_size(workdir):
122
+ spec_path = workdir / "small.json"
123
+ write_json(spec_path, {"slide_size": "4:3",
124
+ "slides": [{"layout": "title", "title": "T"}]})
125
+ out = workdir / "small.pptx"
126
+ run("pptx_create.py", str(spec_path), str(out))
127
+ outline = run("pptx_read.py", str(out))
128
+ assert outline["slide_size_inches"] == [10.0, 7.5]
129
+
130
+
131
+ def test_notes_mode(deck):
132
+ result = run("pptx_read.py", deck, "--notes")
133
+ assert result["notes"][1] == "Keep this under two minutes."
134
+
135
+
136
+ def test_image_export(deck, workdir):
137
+ out_dir = workdir / "exported"
138
+ result = run("pptx_read.py", deck, "--images", str(out_dir))
139
+ assert len(result["exported"]) == 1
140
+ exported = result["exported"][0]
141
+ assert os.path.getsize(exported) > 0
142
+ with open(exported, "rb") as fh:
143
+ assert fh.read(8) == b"\x89PNG\r\n\x1a\n"
144
+
145
+
146
+ def test_template_fill_non_ascii(workdir):
147
+ """Template token fill with non-ASCII values, forced under LC_ALL=C."""
148
+ tpl_spec = workdir / "tpl.json"
149
+ write_json(tpl_spec, {"slides": [
150
+ {"layout": "title", "title": "{{city}} report",
151
+ "subtitle": "Prepared by {{author}}",
152
+ "notes": "Deck for {{city}}."},
153
+ {"layout": "title_content", "title": "Data",
154
+ "tables": [{"rows": [["Site", "{{city}}"]]}]},
155
+ ]})
156
+ template = workdir / "template.pptx"
157
+ run("pptx_create.py", str(tpl_spec), str(template))
158
+
159
+ values = workdir / "values.json"
160
+ write_json(values, {"city": "Z\u00fcrich \u2014 \u2018Bericht\u2019",
161
+ "author": "Beispiel GmbH"})
162
+ out = workdir / "filled.pptx"
163
+ result = run("pptx_from_template.py", str(template), str(out),
164
+ "--values", str(values))
165
+ assert result["tokens_filled"] == 4
166
+
167
+ outline = run("pptx_read.py", str(out))
168
+ expected = "Z\u00fcrich \u2014 \u2018Bericht\u2019"
169
+ assert f"{expected} report" in outline["slides"][0]["texts"]
170
+ assert outline["slides"][0]["notes"] == f"Deck for {expected}."
171
+ assert outline["slides"][1]["tables"][0][0][1] == expected
172
+
173
+
174
+ def test_template_add_slides(workdir):
175
+ template = workdir / "template.pptx"
176
+ add_spec = workdir / "add.json"
177
+ write_json(add_spec, {"slides": [
178
+ {"layout": 1, "title": "Appended", "bullets": ["from layout"],
179
+ "notes": "appended slide"}]})
180
+ out = workdir / "appended.pptx"
181
+ run("pptx_from_template.py", str(template), str(out),
182
+ "--add-slides", str(add_spec))
183
+ outline = run("pptx_read.py", str(out))
184
+ assert outline["slide_count"] == 3
185
+ assert "Appended" in outline["slides"][2]["texts"]
186
+
187
+
188
+ def test_edit_replace_text(deck, workdir):
189
+ edited = workdir / "edited.pptx"
190
+ result = run("pptx_edit.py", deck, "--output", str(edited),
191
+ "--replace-text", "Annual Review", "Semi-Annual Review",
192
+ "--replace-text", "South", "West")
193
+ assert result["replacements"] == 2 # title + table cell
194
+ outline = run("pptx_read.py", str(edited))
195
+ assert "Semi-Annual Review" in outline["slides"][0]["texts"]
196
+ assert outline["slides"][2]["tables"][0][2][0] == "West"
197
+ # formatting survives a run-level replace: red bold bullet untouched
198
+ assert any("Deep dive" in t for t in outline["slides"][1]["texts"])
199
+
200
+
201
+ def test_edit_chart_data(deck, workdir):
202
+ spec = workdir / "chart_update.json"
203
+ write_json(spec, {"slide": 3, "chart": 0,
204
+ "categories": ["Q3", "Q4"],
205
+ "series": {"North": [30, 40], "South": [21, 34]}})
206
+ edited = workdir / "chart_edited.pptx"
207
+ run("pptx_edit.py", deck, "--output", str(edited),
208
+ "--chart-data", str(spec))
209
+ outline = run("pptx_read.py", str(edited))
210
+ bar = outline["slides"][3]["charts"][0]
211
+ assert bar["categories"] == ["Q3", "Q4"]
212
+ north = next(s for s in bar["series"] if s["name"] == "North")
213
+ assert north["values"] == [30.0, 40.0]
214
+
215
+
216
+ def test_edit_remove_and_move_slide(deck, workdir):
217
+ edited = workdir / "reordered.pptx"
218
+ run("pptx_edit.py", deck, "--output", str(edited),
219
+ "--remove-slide", "2", "--move-slide", "2", "0")
220
+ outline = run("pptx_read.py", str(edited))
221
+ assert outline["slide_count"] == 3
222
+ # charts slide (was index 3, then 2 after removal) moved to front
223
+ assert len(outline["slides"][0]["charts"]) == 3
224
+ assert "Annual Review" in outline["slides"][1]["texts"]
225
+
226
+
227
+ def test_edit_swap_image(deck, workdir):
228
+ import base64
229
+ blue = base64.b64decode(
230
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNg"
231
+ "YPj/HwADAgH/p5UronAAAAAASUVORK5CYII=")
232
+ blue_path = workdir / "blue.png"
233
+ blue_path.write_bytes(blue)
234
+ run("pptx_read.py", deck)
235
+ # find picture shape name via python-pptx directly
236
+ sys.path.insert(0, SCRIPTS)
237
+ from pptx import Presentation
238
+ from pptx.enum.shapes import MSO_SHAPE_TYPE
239
+ prs = Presentation(deck)
240
+ name = next(s.name for s in prs.slides[2].shapes
241
+ if s.shape_type == MSO_SHAPE_TYPE.PICTURE)
242
+ edited = workdir / "swapped.pptx"
243
+ run("pptx_edit.py", deck, "--output", str(edited),
244
+ "--swap-image", "2", name, str(blue_path))
245
+ out_dir = workdir / "swapped_images"
246
+ result = run("pptx_read.py", str(edited), "--images", str(out_dir))
247
+ with open(result["exported"][0], "rb") as fh:
248
+ assert fh.read() == blue
249
+
250
+
251
+ def test_help_flags():
252
+ for script in ("pptx_create.py", "pptx_read.py", "pptx_edit.py",
253
+ "pptx_from_template.py"):
254
+ proc = subprocess.run(
255
+ [sys.executable, os.path.join(SCRIPTS, script), "--help"],
256
+ capture_output=True, text=True, encoding="utf-8", check=False)
257
+ assert proc.returncode == 0 and "usage" in proc.stdout.lower()
258
+
259
+
260
+ # ---------------------------------------------------------------------------
261
+ # parity features: render, run-merge replace, chart ops, duplication,
262
+ # polish (background/hyperlink/slide-number/footer), notes editing
263
+ # ---------------------------------------------------------------------------
264
+
265
+ def run_raw(script, *args):
266
+ env = dict(os.environ, LC_ALL="C", PYTHONIOENCODING="utf-8")
267
+ return subprocess.run(
268
+ [sys.executable, os.path.join(SCRIPTS, script), *args],
269
+ capture_output=True, text=True, encoding="utf-8", env=env, check=False)
270
+
271
+
272
+ def test_render_all_slides(workdir):
273
+ import shutil
274
+ spec_path = workdir / "render_spec.json"
275
+ write_json(spec_path, {"slides": [
276
+ {"layout": "title", "title": "One"},
277
+ {"layout": "title_content", "title": "Two", "bullets": ["b"]},
278
+ {"layout": "blank"}]})
279
+ deck3 = workdir / "render_me.pptx"
280
+ run("pptx_create.py", str(spec_path), str(deck3))
281
+ out_dir = workdir / "render_out"
282
+ result = run("pptx_render.py", str(deck3), "--outdir", str(out_dir))
283
+ sys.path.insert(0, SCRIPTS)
284
+ from render import find_soffice
285
+ have_tools = find_soffice() and (
286
+ shutil.which("pdftoppm") or shutil.which("pdftocairo"))
287
+ if have_tools:
288
+ assert result["rendered"] is True
289
+ assert len(result["files"]) == 3
290
+ for png in result["files"]:
291
+ with open(png, "rb") as fh:
292
+ assert fh.read(8) == b"\x89PNG\r\n\x1a\n"
293
+ else:
294
+ assert result["rendered"] is False
295
+ assert result["missing"]
296
+ assert "guidance" in result
297
+
298
+
299
+ def test_replace_across_identically_formatted_runs(workdir):
300
+ """A match split mid-word into equal-format runs keeps formatting."""
301
+ import copy as cp
302
+ from pptx import Presentation
303
+ from pptx.dml.color import RGBColor
304
+ from pptx.util import Inches, Pt
305
+
306
+ prs = Presentation()
307
+ slide = prs.slides.add_slide(prs.slide_layouts[6])
308
+ box = slide.shapes.add_textbox(Inches(1), Inches(1),
309
+ Inches(6), Inches(1))
310
+ para = box.text_frame.paragraphs[0]
311
+ run_obj = para.add_run()
312
+ run_obj.text = "Say TotalWord now"
313
+ run_obj.font.bold = True
314
+ run_obj.font.size = Pt(20)
315
+ run_obj.font.color.rgb = RGBColor.from_string("CC0000")
316
+ # simulate PowerPoint's spell-check split: same rPr, seam mid-match
317
+ second = cp.deepcopy(run_obj._r)
318
+ run_obj._r.addnext(second)
319
+ run_obj.text = "Say Total"
320
+ para.runs[1].text = "Word now"
321
+ path = workdir / "split_runs.pptx"
322
+ prs.save(str(path))
323
+
324
+ edited = workdir / "split_runs_edited.pptx"
325
+ result = run("pptx_edit.py", str(path), "--output", str(edited),
326
+ "--replace-text", "TotalWord", "MergedWord")
327
+ assert result["replacements"] == 1
328
+
329
+ prs2 = Presentation(str(edited))
330
+ para2 = prs2.slides[0].shapes[0].text_frame.paragraphs[0]
331
+ assert "".join(r.text for r in para2.runs) == "Say MergedWord now"
332
+ for r in para2.runs:
333
+ assert r.font.bold is True
334
+ assert r.font.size == Pt(20)
335
+ assert str(r.font.color.rgb) == "CC0000"
336
+
337
+
338
+ def test_chart_surgical_ops(deck, workdir):
339
+ from pptx import Presentation
340
+ spec = workdir / "chart_ops.json"
341
+ write_json(spec, {"slide": 3, "chart": 0, "ops": [
342
+ {"op": "update_series", "name": "North", "values": [99, 88]},
343
+ {"op": "add_series", "name": "East", "values": [1, 2]},
344
+ {"op": "remove_series", "name": "South"},
345
+ {"op": "rename_category", "from": "Q1", "to": "Q1 FY26"},
346
+ {"op": "set_title", "title": "Updated Sales"}]})
347
+ edited = workdir / "chart_ops.pptx"
348
+ run("pptx_edit.py", deck, "--output", str(edited),
349
+ "--chart-data", str(spec))
350
+ outline = run("pptx_read.py", str(edited))
351
+ bar = outline["slides"][3]["charts"][0]
352
+ assert bar["categories"] == ["Q1 FY26", "Q2"]
353
+ names = [s["name"] for s in bar["series"]]
354
+ assert "South" not in names and "East" in names
355
+ north = next(s for s in bar["series"] if s["name"] == "North")
356
+ assert north["values"] == [99.0, 88.0]
357
+ east = next(s for s in bar["series"] if s["name"] == "East")
358
+ assert east["values"] == [1.0, 2.0]
359
+ chart = next(s.chart for s in Presentation(str(edited)).slides[3].shapes
360
+ if s.has_chart)
361
+ assert chart.chart_title.text_frame.text == "Updated Sales"
362
+
363
+
364
+ def test_chart_op_unknown_series_fails(deck, workdir):
365
+ spec = workdir / "chart_bad.json"
366
+ write_json(spec, {"slide": 3, "chart": 0, "ops": [
367
+ {"op": "update_series", "name": "Nowhere", "values": [1, 2]}]})
368
+ proc = run_raw("pptx_edit.py", deck, "--output",
369
+ str(workdir / "never.pptx"), "--chart-data", str(spec))
370
+ assert proc.returncode != 0
371
+ assert "Nowhere" in proc.stderr
372
+
373
+
374
+ def test_duplicate_image_slide_is_independent(deck, workdir):
375
+ import base64
376
+ dup = workdir / "dup.pptx"
377
+ result = run("pptx_edit.py", deck, "--output", str(dup),
378
+ "--duplicate-slide", "2")
379
+ assert result["duplicated_to"] == 4
380
+ outline = run("pptx_read.py", str(dup)) # re-opens cleanly
381
+ assert outline["slide_count"] == 5
382
+ s4 = outline["slides"][4]
383
+ assert len(s4["images"]) == 1
384
+ assert s4["tables"][0][0] == ["Region", "Sales"]
385
+ assert "Callout" in s4["texts"]
386
+
387
+ # independence: swap the image on the COPY, original stays red
388
+ blue = base64.b64decode(
389
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNg"
390
+ "YPj/HwADAgH/p5UronAAAAAASUVORK5CYII=")
391
+ blue_path = workdir / "blue2.png"
392
+ blue_path.write_bytes(blue)
393
+ from pptx import Presentation
394
+ from pptx.enum.shapes import MSO_SHAPE_TYPE
395
+ prs = Presentation(str(dup))
396
+ name = next(s.name for s in prs.slides[4].shapes
397
+ if s.shape_type == MSO_SHAPE_TYPE.PICTURE)
398
+ run("pptx_edit.py", str(dup), "--swap-image", "4", name,
399
+ str(blue_path))
400
+ prs = Presentation(str(dup))
401
+ orig = next(s for s in prs.slides[2].shapes
402
+ if s.shape_type == MSO_SHAPE_TYPE.PICTURE)
403
+ copy_pic = next(s for s in prs.slides[4].shapes
404
+ if s.shape_type == MSO_SHAPE_TYPE.PICTURE)
405
+ assert copy_pic.image.blob == blue
406
+ assert orig.image.blob != blue
407
+
408
+
409
+ def test_duplicate_chart_slide_refused(deck, workdir):
410
+ proc = run_raw("pptx_edit.py", deck, "--output",
411
+ str(workdir / "never2.pptx"), "--duplicate-slide", "3")
412
+ assert proc.returncode != 0
413
+ assert "chart" in proc.stderr.lower()
414
+
415
+
416
+ def test_create_polish_features(workdir):
417
+ from pptx import Presentation
418
+ spec_path = workdir / "polish.json"
419
+ write_json(spec_path, {"slides": [
420
+ {"layout": "title_content", "title": "Polished",
421
+ "background": "112233", "footer": "Confidential draft",
422
+ "slide_number": True,
423
+ "bullets": [{"text": "Visit example",
424
+ "link": "https://example.com/info"}]}]})
425
+ out = workdir / "polish.pptx"
426
+ run("pptx_create.py", str(spec_path), str(out))
427
+ prs = Presentation(str(out))
428
+ slide = prs.slides[0]
429
+ assert str(slide.background.fill.fore_color.rgb) == "112233"
430
+ ph_idx = [ph.placeholder_format.idx for ph in slide.placeholders]
431
+ assert 12 in ph_idx # slide number enabled
432
+ footer = next(ph for ph in slide.placeholders
433
+ if ph.placeholder_format.idx == 11)
434
+ assert footer.text_frame.text == "Confidential draft"
435
+ link_runs = [r for sh in slide.shapes if sh.has_text_frame
436
+ for p in sh.text_frame.paragraphs for r in p.runs
437
+ if r.hyperlink.address]
438
+ assert link_runs[0].hyperlink.address == "https://example.com/info"
439
+
440
+
441
+ def test_edit_polish_features(deck, workdir):
442
+ from pptx import Presentation
443
+ edited = workdir / "polish_edit.pptx"
444
+ result = run("pptx_edit.py", deck, "--output", str(edited),
445
+ "--set-background", "0", "004400",
446
+ "--hyperlink", "1", "Overview", "https://example.com/x",
447
+ "--enable-slide-number", "1",
448
+ "--set-footer", "1", "Footer via edit")
449
+ assert result["background_set"] and result["footer_set"]
450
+ assert result["hyperlinked_runs"] == 1
451
+ assert result["slide_number_enabled"] is True
452
+ prs = Presentation(str(edited))
453
+ assert str(prs.slides[0].background.fill.fore_color.rgb) == "004400"
454
+ s1 = prs.slides[1]
455
+ assert any(ph.placeholder_format.idx == 12 for ph in s1.placeholders)
456
+ footer = next(ph for ph in s1.placeholders
457
+ if ph.placeholder_format.idx == 11)
458
+ assert footer.text_frame.text == "Footer via edit"
459
+ links = [r.hyperlink.address for sh in s1.shapes if sh.has_text_frame
460
+ for p in sh.text_frame.paragraphs for r in p.runs
461
+ if r.hyperlink.address]
462
+ assert links == ["https://example.com/x"]
463
+
464
+
465
+ def test_set_and_append_notes(deck, workdir):
466
+ edited = workdir / "notes_edit.pptx"
467
+ result = run("pptx_edit.py", deck, "--output", str(edited),
468
+ "--set-notes", "0", "Fresh notes")
469
+ assert result["notes_set"]
470
+ run("pptx_edit.py", str(edited), "--append-notes", "0", "Second line")
471
+ notes = run("pptx_read.py", str(edited), "--notes")
472
+ assert notes["notes"][0] == "Fresh notes\nSecond line"
473
+
474
+
475
+ def test_render_help_flag():
476
+ proc = run_raw("pptx_render.py", "--help")
477
+ assert proc.returncode == 0 and "usage" in proc.stdout.lower()
@@ -0,0 +1,114 @@
1
+ ---
2
+ id: skill-create
3
+ name: skill-create
4
+ description: "创建自定义 Skill(SKILL.md),定义可复用的工作流程和指导"
5
+ when_to_use: "用户想创建新的自定义 Skill 时使用"
6
+ version: "1.1.0"
7
+ tags:
8
+ - skill
9
+ - skill-creation
10
+ - workflow
11
+ user_invocable: true
12
+ disable_model_invocation: false
13
+ metadata:
14
+ display-name: "Skill 创建"
15
+ ---
16
+
17
+ # Skill 创建
18
+
19
+ ## 流程
20
+
21
+ 1. **确认需求** — 询问用户:
22
+ - 这个 Skill 做什么?(名称 + 一句话描述)
23
+ - 项目级还是全局?
24
+ - **项目级**: `{workdir}/.cteno/skills/{name}/SKILL.md` — 仅当前项目可用
25
+ - **全局**: 用户 Skill 根目录 `~/.cteno/skills/{name}/SKILL.md`(开发实例为 `~/.cteno-dev/<key>/skills`)— 所有项目可用
26
+ - 用户要和 Claude Code、Codex 等其他 Agent 在同一项目里共用时,改放标准目录 `{workdir}/.agents/skills/{name}/`。Cteno 也读它,同名时 `.cteno/skills` 优先
27
+ - 不要写进 `~/.agents/skills` 或 app 包里的内置目录:前者已不被加载,后者每次升级会被替换
28
+ - 与内置 Skill 同 id 即"覆盖内置",只在用户明确要改写内置行为时这样做;删除这份即恢复内置
29
+
30
+ 2. **设计 Skill** — 根据需求确定:
31
+ - `name`: 小写字母、数字和连字符,≤64 字符,不以连字符开头或结尾,不含连续连字符,**必须与目录名一致**(如 `code-review`、`deploy-staging`)
32
+ - `description`: ≤1024 字符,同时写清"做什么"和"什么时候用",包含触发短语。其他 Agent 只读这个字段决定是否加载
33
+ - 中文显示名放 `metadata.display-name`
34
+ - 步骤拆解:每步必须有**成功标准**
35
+ - 是否需要参数(`$arg_name` 替换)
36
+
37
+ 3. **写入文件** — 用 `shell` 工具创建目录和 SKILL.md
38
+
39
+ 4. **确认** — 告知用户 Skill 已创建,如何激活使用
40
+
41
+ ## SKILL.md 格式
42
+
43
+ ```markdown
44
+ ---
45
+ name: skill-name
46
+ description: "一句话说明做什么,再说明何时使用。例:部署到 staging 环境。用户说部署、发布、上线时使用。"
47
+ # 以下为 Agent Skills 标准可选字段:
48
+ # license: MIT
49
+ # compatibility: "需要 git 和 docker" # 环境要求,≤500 字符
50
+ # allowed-tools: Bash(git:*) Read # 空格分隔,预授权工具(实验性)
51
+ metadata:
52
+ display-name: "显示名称"
53
+ version: "1.0.0"
54
+ # 以下为 Cteno 扩展字段,其他 Agent 会忽略:
55
+ # when_to_use: "更详细的触发说明"
56
+ # user_invocable: true # 用户可通过 skill tool 手动激活(默认 true)
57
+ # disable_model_invocation: false # 禁止模型自动激活(默认 false)
58
+ # argument_hint: "[file] [target]" # 参数提示
59
+ # context: fork # 执行上下文:inline(默认)或 fork(子 agent)
60
+ # agent: worker # fork 时使用的 AgentKind
61
+ ---
62
+
63
+ # Skill 标题
64
+
65
+ 简要描述这个 Skill 做什么。
66
+
67
+ ## 输入(如有参数)
68
+ - `$filename`: 要处理的文件
69
+ - `$target`: 部署目标
70
+
71
+ ## 目标
72
+ 明确的目标和完成标准。最好有具体的交付物。
73
+
74
+ ## 步骤
75
+
76
+ ### 1. 步骤名称
77
+ 具体说明要做什么。包含命令示例。
78
+
79
+ **成功标准**: 如何确认此步完成,可以进入下一步。
80
+
81
+ ### 2. 第二步
82
+ ...
83
+
84
+ ### 3a. 并行步骤 A(与 3b 同时进行)
85
+ ...
86
+
87
+ ### 3b. 并行步骤 B
88
+ ...
89
+
90
+ ### 4. [用户] 确认并审批
91
+ 需要用户介入的步骤用 `[用户]` 标注。用于不可逆操作(合并、发布、删除)。
92
+ ```
93
+
94
+ ## 设计原则
95
+
96
+ - **`description` 是最重要的字段** — 决定模型(包括其他 Agent)何时激活。写清做什么、何时用和触发短语;`when_to_use` 只做补充
97
+ - **只用标准字段就能跨 Agent 使用** — 需要 Cteno 专属行为时再加扩展字段
98
+ - **SKILL.md 控制在 500 行内** — 长参考资料放 `references/`,脚本放 `scripts/`,正文里写相对路径按需读取
99
+ - **每步必须有成功标准** — 模型需要知道何时可以进入下一步
100
+ - **简单 Skill 保持简单** — 2 步的 Skill 不需要在每步加复杂注解
101
+ - **可并行步骤用子编号** — 3a, 3b 表示可同时执行
102
+ - **不可逆操作加 `[用户]`** — 合并 PR、发送消息、删除文件等
103
+
104
+ ## 内置 Skill 参考
105
+
106
+ 默认启用的内置 Skill(避免重复创建;内置 Skill 直接从 app 包读取,可在技能页禁用,`skill-create`、`mcp`、`cteno-host-cli` 除外):
107
+
108
+ | ID | 名称 | 用途 |
109
+ |----|------|------|
110
+ | `skill-create` | Skill 创建 | 创建自定义 Skill |
111
+ | `agent-create` | Agent 创建 | 创建自定义 Agent |
112
+ | `mcp` | MCP Skill | 解析 MCP 文档并自动安装/注册 MCP Server |
113
+ | `cteno-host-cli` | Host CLI | 跨 session 能力的 CLI 指南 |
114
+ | `docx` / `pptx` / `xlsx` / `pdf` | 文档 | Word / PPT / Excel / PDF 创建、编辑与渲染检查 |
@@ -0,0 +1,27 @@
1
+ ---
2
+ id: video-generation
3
+ name: video-generation
4
+ description: "使用独立于聊天模型的视频后端生成、查询与下载视频,保持操作身份并检查实际文件规格。"
5
+ when_to_use: "用户请求生成短视频、动画镜头或使用内置视频模型时。"
6
+ version: "1.1.0"
7
+ tags: [video, generation, media]
8
+ user_invocable: true
9
+ disable_model_invocation: false
10
+ metadata:
11
+ display-name: "视频生成"
12
+ ---
13
+
14
+ # 视频生成
15
+
16
+ - 工具入口是 `generate_video`,后端与协议来自用户配置或托管目录,不从聊天模型或模型名字猜测。
17
+ - 配置单源是 `cteno-host-cli profile --help` 的公共契约,GUI 和工具复用 daemon 能力;不得直接改配置文件或拼供应商 HTTP 绕过工具。
18
+ - 先核对用户目标、时长、画幅、声音和预算。收费生成必须遵守确认门禁,未知价格不猜测。
19
+ - 工具提供 submit/status/download/cancel 契约,但取消须协议验证通过:当前没有已验证的条件取消API,cancel会明确拒绝。Ark DELETE同时可删除终态记录,先GET queued不能排除完成竞态,禁止自行拼DELETE绕过;不假称已取消或退款。
20
+ - submit 只调用一次并保存返回的 operationId。超时或不确定提交禁止用新 identity 重试;查询已有任务或报告待核对。执行器会持久保留提交 claim 防止重复收费。
21
+ - status 查询到 succeeded 后再 download;下载失败只重试下载,绝不重新生成。下载必须由执行 daemon 直取供应商字节,不经过 Happy 服务器中转媒体。
22
+ - 下载成功也不是验收。用 ffprobe 检查实际时长、宽高、音轨,抽帧检查画面与文字;工具返回的规格不是文件事实。
23
+ - 最终交付本地确认过的 artifact 和规格摘要,区分任务成功、计费结算、文件下载和内容验收。没有实际文件不声称交付成功。
24
+ - BYOK 协议只有 `ark-video-tasks`(火山方舟)与 `dashscope-video-synthesis`(阿里云百炼万相异步任务);`openai-videos` 已随 OpenAI 下线不再提供,旧配置只保留可读。
25
+ - 参考素材用 `references: [{ref, role}]`。`ref` 为对话里标注的 `media:<12位hex>` 句柄、工作目录内路径或 https URL,不传 base64。role:`first_frame`/`last_frame`/`reference_image`/`reference_video`/`reference_audio`。
26
+ - 方舟:首尾帧模式不能与 reference_* 混用,尾帧需有首帧,上限 9 图/3 视频/3 音频。万相:仅支持 `first_frame` 与 `reference_audio`(音频限 wan2.5/2.6)。
27
+ - 本地视频/音频文件暂无上传通道,需用 https URL;后端或协议不支持某个 role 时工具报错,不会丢弃参考素材改成纯文生视频,如实转述。
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cteno
4
+ Portions Copyright (c) 2026 Nous Research (scripts/ and tests/ adapted from the Hermes Agent productivity skills)
5
+ Portions Copyright (c) 2026 MiniMaxAI (references adapted from MiniMax-AI/skills)
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
@@ -0,0 +1,8 @@
1
+ # Sources for the office skills (docx, pptx, xlsx, pdf)
2
+
3
+ - **Scripts and tests**: adapted from Nous Research Hermes Agent `skills/productivity/{docx,powerpoint,xlsx,pdf}` (MIT), upstream commit `bf0203a` (2026-10-22).
4
+ - **Chinese typesetting, design and formatting references**: adapted from MiniMax-AI/skills `minimax-docx`, `pptx-generator`, `minimax-xlsx`, `minimax-pdf` (MIT); `xlsx/scripts/formula_check.py` from minimax-xlsx.
5
+ - **Cteno additions**: `scripts/render.py` (isolated LibreOffice profile, macOS CJK font exposure and substitution), `scripts/cjk_fonts.py` (East Asian font slots), CJK handling in `docx_create.py` / `pptx_create.py` / `pdf_create.py`, 16:9 layout stretch in `pptx_create.py`, all `SKILL.md` texts.
6
+ - Anthropic's document skills (proprietary licence) informed which pitfalls to cover; no text or code was copied from them.
7
+
8
+ Not tracked live: to update, diff the upstream folders above against these copies and re-run each skill's `tests/` plus a render check.