claude-code-arcane 2.2.0 → 2.4.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.
Files changed (144) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +104 -55
  3. package/agents/career/career-strategist.md +88 -0
  4. package/agents/career/cv-reviewer.md +81 -0
  5. package/agents/career/hiring-manager.md +68 -0
  6. package/agents/career/mock-interviewer.md +76 -0
  7. package/agents/freelance/client-screener.md +84 -0
  8. package/agents/freelance/discovery-call.md +65 -0
  9. package/agents/freelance/pipeline-strategist.md +99 -0
  10. package/agents/freelance/proposal-reviewer.md +82 -0
  11. package/agents/game/ue-blueprint-specialist.md +125 -0
  12. package/agents/game/ue-gas-specialist.md +133 -0
  13. package/agents/game/ue-umg-specialist.md +131 -0
  14. package/dist/cli.js +57 -13
  15. package/docs/RELEASE-SETUP.md +1 -1
  16. package/docs/SKILLS-CATALOG.md +67 -4
  17. package/docs/USER-GUIDE.md +77 -72
  18. package/docs/agent-hierarchy.md +21 -1
  19. package/docs/presentations/build_arcane_deck.py +2 -2
  20. package/docs/presentations/build_arcane_deck_12.py +2 -2
  21. package/hooks/detect-unreal.sh +90 -0
  22. package/package.json +2 -2
  23. package/profiles/freelance.yaml +43 -0
  24. package/profiles/job-hunt.yaml +5 -2
  25. package/profiles/unreal-dev.yaml +97 -0
  26. package/rules/drafter-reviewer.md +16 -3
  27. package/rules/freelance-guardrails.md +53 -0
  28. package/rules/gamedev/unreal-code.md +125 -0
  29. package/skills/career-registry/SKILL.md +95 -0
  30. package/skills/career-registry/scripts/career_registry.py +488 -0
  31. package/skills/client-screen/SKILL.md +83 -0
  32. package/skills/client-screen/references/red-flags.md +95 -0
  33. package/skills/cover-letter/SKILL.md +4 -3
  34. package/skills/cv-tailor/SKILL.md +11 -2
  35. package/skills/freelance-deliver/SKILL.md +90 -0
  36. package/skills/freelance-hunt/SKILL.md +126 -0
  37. package/skills/freelance-hunt/references/templates/Connects.md +39 -0
  38. package/skills/freelance-hunt/references/templates/Contrato.md +60 -0
  39. package/skills/freelance-hunt/references/templates/Propuesta.md +66 -0
  40. package/skills/freelance-kickoff/SKILL.md +94 -0
  41. package/skills/freelance-pipeline/SKILL.md +109 -0
  42. package/skills/freelance-pipeline/scripts/import_history.py +425 -0
  43. package/skills/freelance-profile/SKILL.md +93 -0
  44. package/skills/freelance-proposal/SKILL.md +112 -0
  45. package/skills/freelance-scan/SKILL.md +196 -0
  46. package/skills/freelance-scan/references/dedup.md +35 -0
  47. package/skills/freelance-scan/references/platforms.md +167 -0
  48. package/skills/freelance-scan/scripts/freelance_search.py +1077 -0
  49. package/skills/install-mcp/SKILL.md +135 -46
  50. package/skills/install-mcp/references/unreal-operations.md +55 -0
  51. package/skills/interview-prep/SKILL.md +11 -3
  52. package/skills/job-aplicar/SKILL.md +5 -1
  53. package/skills/job-hunt/SKILL.md +17 -0
  54. package/skills/job-outcome/SKILL.md +15 -1
  55. package/skills/job-upskill/SKILL.md +10 -2
  56. package/skills/ue-actor-component-architecture/SKILL.md +509 -0
  57. package/skills/ue-actor-component-architecture/references/actor-lifecycle.md +301 -0
  58. package/skills/ue-actor-component-architecture/references/component-types.md +592 -0
  59. package/skills/ue-ai-navigation/SKILL.md +526 -0
  60. package/skills/ue-ai-navigation/references/behavior-tree-patterns.md +392 -0
  61. package/skills/ue-ai-navigation/references/behavior-tree-workflow.md +107 -0
  62. package/skills/ue-ai-navigation/references/custom-bttask.md +104 -0
  63. package/skills/ue-ai-navigation/references/eqs-reference.md +423 -0
  64. package/skills/ue-animation-system/SKILL.md +521 -0
  65. package/skills/ue-animation-system/references/anim-notify-reference.md +486 -0
  66. package/skills/ue-animation-system/references/locomotion-setup.md +550 -0
  67. package/skills/ue-async-threading/SKILL.md +433 -0
  68. package/skills/ue-async-threading/references/thread-safety-guide.md +213 -0
  69. package/skills/ue-async-threading/references/threading-patterns.md +345 -0
  70. package/skills/ue-audio-system/SKILL.md +527 -0
  71. package/skills/ue-audio-system/references/audio-setup-patterns.md +700 -0
  72. package/skills/ue-blueprints/SKILL.md +134 -0
  73. package/skills/ue-blueprints/references/communication.md +82 -0
  74. package/skills/ue-character-movement/SKILL.md +475 -0
  75. package/skills/ue-character-movement/references/cmc-extension-patterns.md +335 -0
  76. package/skills/ue-character-movement/references/movement-pipeline.md +189 -0
  77. package/skills/ue-cpp-foundations/SKILL.md +533 -0
  78. package/skills/ue-cpp-foundations/references/components-and-gc.md +107 -0
  79. package/skills/ue-cpp-foundations/references/container-patterns.md +544 -0
  80. package/skills/ue-cpp-foundations/references/delegate-patterns.md +497 -0
  81. package/skills/ue-cpp-foundations/references/gameplay-classes-cpp.md +136 -0
  82. package/skills/ue-cpp-foundations/references/property-specifiers.md +306 -0
  83. package/skills/ue-data-assets-tables/SKILL.md +527 -0
  84. package/skills/ue-data-assets-tables/references/asset-loading-patterns.md +306 -0
  85. package/skills/ue-data-assets-tables/references/data-driven-design.md +378 -0
  86. package/skills/ue-editor-tools/SKILL.md +523 -0
  87. package/skills/ue-editor-tools/references/detail-customization-patterns.md +395 -0
  88. package/skills/ue-editor-tools/references/editor-module-setup.md +480 -0
  89. package/skills/ue-game-features/SKILL.md +477 -0
  90. package/skills/ue-game-features/references/experience-system.md +222 -0
  91. package/skills/ue-game-features/references/game-feature-patterns.md +308 -0
  92. package/skills/ue-gameplay-abilities/SKILL.md +518 -0
  93. package/skills/ue-gameplay-abilities/references/ability-task-reference.md +391 -0
  94. package/skills/ue-gameplay-abilities/references/gameplay-effect-reference.md +397 -0
  95. package/skills/ue-gameplay-abilities/references/gas-setup-patterns.md +369 -0
  96. package/skills/ue-gameplay-framework/SKILL.md +466 -0
  97. package/skills/ue-gameplay-framework/references/framework-class-map.md +243 -0
  98. package/skills/ue-input-system/SKILL.md +394 -0
  99. package/skills/ue-input-system/references/cpp-setup.md +131 -0
  100. package/skills/ue-input-system/references/enhanced-input-workflow.md +117 -0
  101. package/skills/ue-input-system/references/input-action-reference.md +731 -0
  102. package/skills/ue-mass-entity/SKILL.md +476 -0
  103. package/skills/ue-mass-entity/references/mass-entity-patterns.md +319 -0
  104. package/skills/ue-mass-entity/references/mass-fragment-reference.md +119 -0
  105. package/skills/ue-materials-rendering/SKILL.md +526 -0
  106. package/skills/ue-materials-rendering/references/material-parameter-reference.md +216 -0
  107. package/skills/ue-materials-rendering/references/post-process-settings.md +383 -0
  108. package/skills/ue-module-build-system/SKILL.md +487 -0
  109. package/skills/ue-module-build-system/references/build-cs-reference.md +418 -0
  110. package/skills/ue-module-build-system/references/common-build-errors.md +381 -0
  111. package/skills/ue-naming-conventions/SKILL.md +142 -0
  112. package/skills/ue-naming-conventions/references/asset-name-modifiers.md +233 -0
  113. package/skills/ue-naming-conventions/references/content-structure.md +83 -0
  114. package/skills/ue-networking-replication/SKILL.md +452 -0
  115. package/skills/ue-networking-replication/references/replication-patterns.md +526 -0
  116. package/skills/ue-networking-replication/references/rpc-decision-guide.md +393 -0
  117. package/skills/ue-niagara-effects/SKILL.md +531 -0
  118. package/skills/ue-niagara-effects/references/niagara-data-interfaces.md +491 -0
  119. package/skills/ue-niagara-effects/references/niagara-parameter-types.md +153 -0
  120. package/skills/ue-niagara-effects/references/niagara-workflow.md +115 -0
  121. package/skills/ue-packaging/SKILL.md +116 -0
  122. package/skills/ue-physics-collision/SKILL.md +423 -0
  123. package/skills/ue-physics-collision/references/collision-channel-setup.md +218 -0
  124. package/skills/ue-physics-collision/references/trace-patterns.md +499 -0
  125. package/skills/ue-procedural-generation/SKILL.md +505 -0
  126. package/skills/ue-procedural-generation/references/pcg-node-reference.md +381 -0
  127. package/skills/ue-procedural-generation/references/procedural-mesh-patterns.md +940 -0
  128. package/skills/ue-project-context/SKILL.md +305 -0
  129. package/skills/ue-sequencer-cinematics/SKILL.md +506 -0
  130. package/skills/ue-sequencer-cinematics/references/sequencer-patterns.md +629 -0
  131. package/skills/ue-serialization-savegames/SKILL.md +524 -0
  132. package/skills/ue-serialization-savegames/references/save-system-architecture.md +705 -0
  133. package/skills/ue-state-trees/SKILL.md +489 -0
  134. package/skills/ue-state-trees/references/state-tree-mass-integration.md +184 -0
  135. package/skills/ue-state-trees/references/state-tree-patterns.md +321 -0
  136. package/skills/ue-testing-debugging/SKILL.md +520 -0
  137. package/skills/ue-testing-debugging/references/automation-test-patterns.md +387 -0
  138. package/skills/ue-testing-debugging/references/profiling-commands.md +357 -0
  139. package/skills/ue-ui-umg-slate/SKILL.md +431 -0
  140. package/skills/ue-ui-umg-slate/references/common-ui-setup.md +484 -0
  141. package/skills/ue-ui-umg-slate/references/widget-types.md +487 -0
  142. package/skills/ue-world-level-streaming/SKILL.md +518 -0
  143. package/skills/ue-world-level-streaming/references/streaming-patterns.md +414 -0
  144. package/templates/gamedev/unreal-project-context.md +107 -0
@@ -0,0 +1,425 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Importa historial de facturacion freelance desde un CSV que exportaste vos.
4
+
5
+ Mismo patron que job-scrape/network_map.py: no toca tu cuenta ni pide
6
+ credenciales. Vos bajas el CSV desde la plataforma y este script lo lee. Sin API
7
+ key, sin login, sin scrapear.
8
+
9
+ Por que detecta las columnas en vez de asumirlas: Upwork tiene al menos dos
10
+ exports distintos (Transaction History y Contract History), sus nombres de
11
+ columna no estan documentados publicamente, y cambian. Hardcodear un esquema que
12
+ no se puede verificar produce justo el codigo que falla en la primera corrida
13
+ real del usuario. Ademas asi sirve para cualquier otra fuente: Freelancer.com, un
14
+ export de tu banco, o la planilla que ya venias llevando a mano.
15
+
16
+ El contrato con el usuario es explicito: `inspect` muestra que columna se mapeo a
17
+ que campo y **que columnas no se pudieron mapear**, para que el usuario corrija
18
+ con --map antes de creerle a los numeros.
19
+
20
+ Uso:
21
+ python import_history.py inspect transacciones.csv
22
+ python import_history.py summary transacciones.csv
23
+ python import_history.py summary transacciones.csv --map "Importe=monto" --map "Horas trabajadas=horas"
24
+ python import_history.py import transacciones.csv
25
+
26
+ Read-only: nunca escribe el CSV ni las notas. Solo stdlib. Python 3.9.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import csv
33
+ import json
34
+ import re
35
+ import sys
36
+ import unicodedata
37
+ from collections import OrderedDict
38
+ from datetime import datetime
39
+
40
+ # Campos normalizados y los nombres reales con los que aparecen. El orden importa:
41
+ # se prueba coincidencia exacta primero y despues por substring, asi que los
42
+ # alias mas especificos van antes.
43
+ FIELD_ALIASES = OrderedDict([
44
+ ("fecha", ["date", "fecha", "transaction date", "date/time", "period",
45
+ "start date", "created", "week"]),
46
+ ("tipo", ["type", "tipo", "transaction type", "category", "categoria"]),
47
+ ("cliente", ["client", "cliente", "company", "empresa", "team",
48
+ "contract team", "buyer", "payer"]),
49
+ ("contrato", ["contract title", "contract", "contrato", "description",
50
+ "descripcion", "memo", "project", "proyecto", "job title"]),
51
+ # `fee` y `neto` van antes de `monto`: "amount" hace substring con casi todo,
52
+ # y sin este orden una columna de fee cae en monto.
53
+ ("fee", ["freelancer service fee", "service fee", "upwork fee", "fee",
54
+ "comision", "commission", "marketplace fee"]),
55
+ ("neto", ["net amount", "net", "neto", "take home", "payout", "liquido"]),
56
+ ("monto", ["amount", "monto", "importe", "total", "gross", "bruto",
57
+ "earnings", "ganancia"]),
58
+ ("horas", ["hours", "horas", "quantity", "qty", "cantidad", "duration",
59
+ "time", "hs"]),
60
+ ("moneda", ["currency", "moneda", "curr"]),
61
+ ])
62
+
63
+ # Tipos de transaccion que NO son ingreso. Un export trae de todo mezclado y
64
+ # sumar sin filtrar da un total inflado.
65
+ NON_INCOME_HINTS = ("withdrawal", "retiro", "transfer", "transferencia",
66
+ "membership", "connects", "refund", "reembolso",
67
+ "adjustment", "ajuste", "tax", "impuesto")
68
+
69
+ # En el transaction history de Upwork la comision NO es una columna: es una fila
70
+ # aparte con monto negativo y tipo "Service Fee". Sin detectarlas por fila, el
71
+ # total sale correcto pero etiquetado como bruto, y comisiones queda en cero -
72
+ # o sea, el reporte miente sobre de donde salio el numero.
73
+ FEE_ROW_HINTS = ("service fee", "freelancer fee", "upwork fee", "comision",
74
+ "commission", "marketplace fee", "processing fee")
75
+
76
+ DATE_FORMATS = ("%Y-%m-%d", "%d/%m/%Y", "%m/%d/%Y", "%Y/%m/%d",
77
+ "%b %d, %Y", "%d %b %Y", "%Y-%m-%d %H:%M:%S", "%m/%d/%y")
78
+
79
+ MIN_ROWS_FOR_RATES = 5
80
+
81
+
82
+ def fold(text: str) -> str:
83
+ decomposed = unicodedata.normalize("NFD", str(text or "").lower())
84
+ return "".join(c for c in decomposed if unicodedata.category(c) != "Mn")
85
+
86
+
87
+ def normalize_header(name: str) -> str:
88
+ return re.sub(r"\s+", " ", re.sub(r"[^a-z0-9 ]+", " ", fold(name))).strip()
89
+
90
+
91
+ def parse_money(raw):
92
+ """
93
+ Plata en cualquiera de los formatos que aparecen en un export real.
94
+
95
+ Casos que se vieron y hay que aguantar: "$1,234.56", "1.234,56" (europeo),
96
+ "(45.00)" negativo entre parentesis (convencion contable), "-", vacio, y
97
+ sufijos de moneda tipo "120.00 USD".
98
+ """
99
+ text = str(raw or "").strip()
100
+ if not text or text in ("-", "--", "n/a", "N/A"):
101
+ return None
102
+ negative = text.startswith("(") and text.endswith(")")
103
+ text = re.sub(r"[()]", "", text)
104
+ text = re.sub(r"[^\d.,\-]", "", text)
105
+ if not text or text in ("-", ".", ","):
106
+ return None
107
+ # Formato europeo: la coma es el decimal si es el ultimo separador.
108
+ if "," in text and "." in text:
109
+ if text.rfind(",") > text.rfind("."):
110
+ text = text.replace(".", "").replace(",", ".")
111
+ else:
112
+ text = text.replace(",", "")
113
+ elif "," in text:
114
+ # Coma sola: decimal si deja 1 o 2 digitos atras, si no es de miles.
115
+ tail = text.split(",")[-1]
116
+ text = text.replace(",", "." if len(tail) in (1, 2) else "")
117
+ try:
118
+ value = float(text)
119
+ except ValueError:
120
+ return None
121
+ return -value if negative else value
122
+
123
+
124
+ def parse_hours(raw):
125
+ """Horas: acepta decimal y tambien "3:30" que algunos exports usan."""
126
+ text = str(raw or "").strip()
127
+ if not text:
128
+ return None
129
+ match = re.match(r"^(\d+):([0-5]\d)$", text)
130
+ if match:
131
+ return int(match.group(1)) + int(match.group(2)) / 60.0
132
+ return parse_money(text)
133
+
134
+
135
+ def parse_date(raw):
136
+ text = str(raw or "").strip()
137
+ if not text:
138
+ return None
139
+ for fmt in DATE_FORMATS:
140
+ try:
141
+ return datetime.strptime(text, fmt).date()
142
+ except ValueError:
143
+ continue
144
+ return None
145
+
146
+
147
+ def map_columns(headers, overrides=None):
148
+ """
149
+ Devuelve (mapeo, sin_mapear, ambiguos).
150
+
151
+ Ambiguo = dos columnas quieren el mismo campo. No se elige en silencio: se
152
+ reporta, porque elegir mal ahi cambia todos los numeros de abajo.
153
+ """
154
+ overrides = overrides or {}
155
+ mapping, claims = {}, {}
156
+
157
+ # Los overrides se procesan primero y bloquean el campo: `--map` existe para
158
+ # ganarle a la deteccion, y si se mezclaran en el mismo recorrido ganaria la
159
+ # columna que aparece antes en el header, que es lo contrario de lo pedido.
160
+ forced = set()
161
+ for header in headers:
162
+ if header in overrides:
163
+ field = overrides[header]
164
+ claims[field] = [header]
165
+ forced.add(field)
166
+
167
+ for header in headers:
168
+ if header in overrides:
169
+ continue
170
+ norm = normalize_header(header)
171
+ if not norm:
172
+ continue
173
+ chosen = None
174
+ for field, aliases in FIELD_ALIASES.items():
175
+ if field in forced:
176
+ continue
177
+ if norm in [normalize_header(a) for a in aliases]:
178
+ chosen = field
179
+ break
180
+ if not chosen:
181
+ for field, aliases in FIELD_ALIASES.items():
182
+ if field in forced:
183
+ continue
184
+ if any(normalize_header(a) in norm for a in aliases):
185
+ chosen = field
186
+ break
187
+ if chosen:
188
+ claims.setdefault(chosen, []).append(header)
189
+
190
+ ambiguous = {}
191
+ for field, columns in claims.items():
192
+ if len(columns) > 1:
193
+ ambiguous[field] = columns
194
+ mapping[field] = columns[0]
195
+
196
+ mapped = set(mapping.values())
197
+ unmapped = [h for h in headers if h not in mapped and normalize_header(h)]
198
+ return mapping, unmapped, ambiguous
199
+
200
+
201
+ def read_csv(path):
202
+ """Lee el CSV saltando el preambulo de aviso que algunos exports ponen arriba."""
203
+ with open(path, "r", encoding="utf-8-sig", errors="replace", newline="") as handle:
204
+ lines = handle.read().splitlines()
205
+ if not lines:
206
+ raise ValueError("el archivo esta vacio")
207
+
208
+ # Heuristica: la fila de header es la primera con al menos 3 separadores.
209
+ start = 0
210
+ for index, line in enumerate(lines[:20]):
211
+ if line.count(",") >= 2 or line.count(";") >= 2:
212
+ start = index
213
+ break
214
+ sample = "\n".join(lines[start:start + 5])
215
+ try:
216
+ dialect = csv.Sniffer().sniff(sample, delimiters=",;\t")
217
+ except csv.Error:
218
+ dialect = csv.excel
219
+ reader = csv.DictReader(lines[start:], dialect=dialect)
220
+ headers = [h for h in (reader.fieldnames or []) if h is not None]
221
+ return headers, list(reader), start
222
+
223
+
224
+ def normalize_rows(rows, mapping):
225
+ out = []
226
+ for raw in rows:
227
+ def get(field):
228
+ column = mapping.get(field)
229
+ return raw.get(column) if column else None
230
+
231
+ monto = parse_money(get("monto"))
232
+ fee = parse_money(get("fee"))
233
+ neto = parse_money(get("neto"))
234
+ # Si el export trae bruto y fee pero no neto, se deriva. El fee suele
235
+ # venir negativo, asi que se usa el valor absoluto.
236
+ if neto is None and monto is not None and fee is not None:
237
+ neto = monto - abs(fee)
238
+
239
+ tipo = str(get("tipo") or "").strip()
240
+ folded = fold(tipo)
241
+ es_fila_de_fee = any(h in folded for h in FEE_ROW_HINTS)
242
+ out.append({
243
+ "fecha": parse_date(get("fecha")),
244
+ "tipo": tipo,
245
+ "es_fila_de_fee": es_fila_de_fee,
246
+ "es_ingreso": not es_fila_de_fee and not any(h in folded for h in NON_INCOME_HINTS),
247
+ "cliente": str(get("cliente") or "").strip(),
248
+ "contrato": str(get("contrato") or "").strip(),
249
+ "monto": monto,
250
+ "fee": fee,
251
+ "neto": neto if neto is not None else monto,
252
+ "horas": parse_hours(get("horas")),
253
+ "moneda": str(get("moneda") or "").strip(),
254
+ })
255
+ return out
256
+
257
+
258
+ def summarize(rows):
259
+ income = [r for r in rows if r["es_ingreso"]]
260
+ fee_rows = [r for r in rows if r.get("es_fila_de_fee")]
261
+ montos = [r["monto"] for r in income if r["monto"] is not None]
262
+ # Las comisiones pueden venir por columna (una por movimiento) o por fila
263
+ # aparte. Se suman las dos formas; en un export dado hay una sola.
264
+ fees = [abs(r["fee"]) for r in income if r["fee"] is not None]
265
+ fees += [abs(r["monto"]) for r in fee_rows if r["monto"] is not None]
266
+ # El neto se deriva del bruto menos comisiones cuando estas vienen por fila,
267
+ # porque en ese caso la columna `neto` de cada movimiento no las conoce.
268
+ netos = [r["neto"] for r in income if r["neto"] is not None]
269
+ horas = [r["horas"] for r in income if r["horas"] is not None]
270
+ fechas = sorted(r["fecha"] for r in rows if r["fecha"])
271
+
272
+ por_cliente = {}
273
+ for row in income:
274
+ key = row["cliente"] or "(sin cliente)"
275
+ bucket = por_cliente.setdefault(key, {"neto": 0.0, "horas": 0.0, "movimientos": 0})
276
+ bucket["neto"] += row["neto"] or 0
277
+ bucket["horas"] += row["horas"] or 0
278
+ bucket["movimientos"] += 1
279
+
280
+ total_bruto = sum(montos)
281
+ total_fees = sum(fees)
282
+ total_neto = (total_bruto - total_fees) if fee_rows else sum(netos)
283
+ ranking = sorted(por_cliente.items(), key=lambda kv: kv[1]["neto"], reverse=True)
284
+ concentracion = None
285
+ if total_neto and ranking:
286
+ concentracion = {
287
+ "cliente_mas_grande": ranking[0][0],
288
+ "porcentaje_del_ingreso": round(ranking[0][1]["neto"] / total_neto * 100, 1),
289
+ }
290
+
291
+ # La tarifa efectiva es el numero que dice si el negocio funciona, pero
292
+ # solo se puede calcular si el export trae horas. Sin horas se dice que
293
+ # falta el dato, no se estima.
294
+ tarifa_efectiva = None
295
+ if horas and sum(horas) > 0 and total_neto:
296
+ tarifa_efectiva = round(total_neto / sum(horas), 2)
297
+
298
+ return {
299
+ "movimientos_leidos": len(rows),
300
+ "movimientos_de_ingreso": len(income),
301
+ "filas_de_comision": len(fee_rows),
302
+ "excluidos_por_tipo": len(rows) - len(income) - len(fee_rows),
303
+ "rango_de_fechas": [str(fechas[0]), str(fechas[-1])] if fechas else None,
304
+ "bruto": round(total_bruto, 2) if montos else None,
305
+ "comisiones": round(total_fees, 2) if fees else None,
306
+ "neto": round(total_neto, 2) if (netos or fee_rows) else None,
307
+ "horas_registradas": round(sum(horas), 2) if horas else None,
308
+ "tarifa_efectiva_por_hora": tarifa_efectiva,
309
+ "tarifa_efectiva_calculable": tarifa_efectiva is not None,
310
+ "por_cliente": OrderedDict(
311
+ (name, {"neto": round(v["neto"], 2), "horas": round(v["horas"], 2),
312
+ "movimientos": v["movimientos"]})
313
+ for name, v in ranking),
314
+ "concentracion": concentracion,
315
+ "muestra_suficiente": len(income) >= MIN_ROWS_FOR_RATES,
316
+ }
317
+
318
+
319
+ # --------------------------------------------------------------------------- #
320
+
321
+ def load(args):
322
+ headers, rows, skipped = read_csv(args.csv)
323
+ overrides = {}
324
+ for item in (args.map or []):
325
+ if "=" not in item:
326
+ raise ValueError("--map espera 'Columna=campo', recibi: {0}".format(item))
327
+ column, field = item.split("=", 1)
328
+ field = field.strip()
329
+ if field not in FIELD_ALIASES:
330
+ raise ValueError("campo desconocido '{0}'; validos: {1}".format(
331
+ field, ", ".join(FIELD_ALIASES)))
332
+ overrides[column.strip()] = field
333
+ mapping, unmapped, ambiguous = map_columns(headers, overrides)
334
+ return headers, rows, skipped, mapping, unmapped, ambiguous
335
+
336
+
337
+ def run_inspect(args) -> int:
338
+ headers, rows, skipped, mapping, unmapped, ambiguous = load(args)
339
+ faltantes = [f for f in ("fecha", "monto") if f not in mapping]
340
+ emit({
341
+ "archivo": args.csv,
342
+ "filas_de_preambulo_salteadas": skipped,
343
+ "columnas_encontradas": headers,
344
+ "mapeo": mapping,
345
+ "columnas_sin_mapear": unmapped,
346
+ "columnas_ambiguas": ambiguous,
347
+ "campos_criticos_faltantes": faltantes,
348
+ "usable": not faltantes,
349
+ "ejemplo_normalizado": normalize_rows(rows[:2], mapping),
350
+ "como_corregir": "--map \"Nombre exacto de la columna=campo\"; campos: {0}".format(
351
+ ", ".join(FIELD_ALIASES)),
352
+ })
353
+ return 0 if not faltantes else 1
354
+
355
+
356
+ def run_summary(args) -> int:
357
+ _, rows, _, mapping, unmapped, ambiguous = load(args)
358
+ if "monto" not in mapping:
359
+ emit({"error": "no encontre la columna de monto; corre `inspect` y usa --map",
360
+ "code": "NO_AMOUNT_COLUMN"})
361
+ return 1
362
+ payload = summarize(normalize_rows(rows, mapping))
363
+ payload["mapeo_usado"] = mapping
364
+ # Se repiten aca a proposito: quien mira el summary tiene que ver si el
365
+ # mapeo dejo algo afuera antes de creerle a los totales.
366
+ payload["columnas_sin_mapear"] = unmapped
367
+ payload["columnas_ambiguas"] = ambiguous
368
+ emit(payload)
369
+ return 0
370
+
371
+
372
+ def run_import(args) -> int:
373
+ _, rows, _, mapping, unmapped, ambiguous = load(args)
374
+ emit({
375
+ "mapeo_usado": mapping,
376
+ "columnas_sin_mapear": unmapped,
377
+ "columnas_ambiguas": ambiguous,
378
+ "movimientos": normalize_rows(rows, mapping),
379
+ })
380
+ return 0
381
+
382
+
383
+ def emit(payload) -> None:
384
+ json.dump(payload, sys.stdout, ensure_ascii=False, indent=2, default=str)
385
+ sys.stdout.write("\n")
386
+
387
+
388
+ def force_utf8_streams() -> None:
389
+ """En Windows stdout es cp1252 y un nombre con acento revienta al pipear."""
390
+ for stream in (sys.stdout, sys.stderr):
391
+ if hasattr(stream, "reconfigure"):
392
+ stream.reconfigure(encoding="utf-8")
393
+
394
+
395
+ def build_parser():
396
+ parser = argparse.ArgumentParser(
397
+ description="Importa historial de facturacion freelance desde un CSV exportado a mano.")
398
+ sub = parser.add_subparsers(dest="command", required=True)
399
+ for name, func, help_text in (
400
+ ("inspect", run_inspect, "mostrar que columnas detecto y cuales no pudo mapear"),
401
+ ("summary", run_summary, "totales, tarifa efectiva y concentracion de clientes"),
402
+ ("import", run_import, "volcar los movimientos normalizados en JSON"),
403
+ ):
404
+ cmd = sub.add_parser(name, help=help_text)
405
+ cmd.add_argument("csv", help="ruta del CSV exportado")
406
+ cmd.add_argument("--map", action="append",
407
+ help="forzar un mapeo: \"Columna=campo\" (repetible)")
408
+ cmd.set_defaults(func=func)
409
+ return parser
410
+
411
+
412
+ def main(argv=None) -> int:
413
+ force_utf8_streams()
414
+ args = build_parser().parse_args(argv)
415
+ try:
416
+ return args.func(args)
417
+ except (OSError, ValueError) as e:
418
+ emit({"error": str(e), "code": "BAD_INPUT"})
419
+ return 1
420
+ except KeyboardInterrupt:
421
+ return 130
422
+
423
+
424
+ if __name__ == "__main__":
425
+ sys.exit(main())
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: freelance-profile
3
+ description: "Build the Upwork profile that decides whether clients ever open your proposal: title, the first 250 visible characters of the overview, specialized profiles, portfolio items and rate. Triggers: perfil de upwork, optimizar perfil freelance, titulo de upwork, overview upwork, specialized profile, que tarifa pongo en upwork."
4
+ argument-hint: "[audit | title | overview | portfolio | rate]"
5
+ category: "career"
6
+ user-invocable: true
7
+ allowed-tools: Read, Glob, Grep, Write, Edit, WebFetch
8
+ ---
9
+
10
+ # Upwork Profile - El perfil de la plataforma
11
+
12
+ Construís el perfil de Upwork desde el perfil maestro. No es un CV: el CV lo lee alguien que ya decidió considerarte, el perfil de Upwork lo lee alguien que está barriendo una lista de 40 freelancers o revisando propuestas.
13
+
14
+ Rutas relativas al career workspace (`--workspace`, env `CAREER_WORKSPACE`, o `./career-workspace/`).
15
+
16
+ Modo: `$ARGUMENTS`
17
+
18
+ ## Inputs
19
+
20
+ - **El perfil maestro** de `01-Perfiles/` - la fuente de verdad. Si no existe, correr `/master-profile` primero.
21
+ - **El perfil actual de Upwork**, si ya existe: URL pública → WebFetch, o pegado.
22
+ - **Contratos cerrados** de `08-Contratos/` para los portfolio items con resultados reales.
23
+
24
+ ## Modos
25
+
26
+ ### `audit` - Revisar el perfil actual
27
+
28
+ Recorrer los cinco elementos de abajo y reportar qué falta o qué está flojo, priorizado por impacto. Empezar siempre por título y las primeras 250 caracteres del overview: son los únicos que se ven sin hacer click.
29
+
30
+ ### `title` - El título
31
+
32
+ Lo primero y a veces lo único que se lee. Reglas:
33
+
34
+ - **Nicho específico, no rol genérico.** "Full Stack Developer" compite con 200 mil personas. "Unity Developer - Gameplay & Multiplayer for Mobile" compite con pocas y aparece en las búsquedas que importan.
35
+ - Incluir la tecnología o el resultado que el cliente busca en sus palabras, no en las tuyas.
36
+ - Sin "Ninja", "Rockstar", "Guru".
37
+ - Sin declaraciones de disponibilidad ("Available now") - ocupan lugar y caducan.
38
+
39
+ ### `overview` - Los primeros 250 caracteres
40
+
41
+ El overview se corta en la vista previa. Todo lo que importa va en el primer bloque.
42
+
43
+ Estructura:
44
+
45
+ 1. **Primera línea**: a quién ayudás y con qué resultado. No "I am a developer with 5 years of experience" - eso describe tu currículum, no el problema del cliente.
46
+ 2. **Segundo bloque**: la prueba más fuerte, con número.
47
+ 3. **Después del corte**: stack, cómo trabajás, tipos de proyecto que tomás, y qué **no** tomás. Decir qué no hacés filtra clientes malos y sube la tasa de respuesta de los buenos.
48
+ 4. **Cierre**: un CTA simple.
49
+
50
+ Escribir en primera persona y en el idioma de tu mercado objetivo. Si apuntás a clientes de EEUU, en inglés.
51
+
52
+ ### `portfolio` - Los items
53
+
54
+ - Cada item es **un problema, lo que hiciste y el resultado con número**. No una captura de pantalla linda sin contexto.
55
+ - Priorizar los que se parecen a los trabajos que querés conseguir, no los que más te gustaron.
56
+ - Sacar los que no representan el nicho del título: un portfolio disperso comunica "hago cualquier cosa", que es lo contrario de lo que querés.
57
+ - Nunca subir trabajo de un cliente sin permiso, ni material bajo NDA. Si no se puede mostrar, se describe el problema y el resultado sin identificar al cliente.
58
+
59
+ ### `rate` - La tarifa publicada
60
+
61
+ - Se deriva del **piso neto** de `01-Perfiles/` más la comisión de la plataforma, no de lo que cobran otros.
62
+ - La tarifa publicada es un filtro, no un compromiso: filtra el tráfico que llega, y podés cotizar distinto por proyecto en cada propuesta.
63
+ - Poner una tarifa muy baja para "arrancar" atrae clientes que buscan precio bajo, que son los que más problemas dan y peor califican. Subirla después es más difícil que empezar bien.
64
+ - Si el usuario está arrancando sin historial, la palanca correcta es acotar el nicho, no bajar el precio.
65
+
66
+ ## Specialized profiles
67
+
68
+ Upwork permite perfiles especializados además del general. Sirven cuando el usuario tiene dos nichos con vocabulario distinto (ej. "Unity gameplay" y "backend .NET"): cada uno se presenta a su búsqueda sin diluir al otro.
69
+
70
+ Si hay un solo nicho, no crear especializados: sumar superficie vacía baja la señal.
71
+
72
+ ## Lo que el perfil no controla
73
+
74
+ El **Job Success Score** y el badge no se editan: salen de cómo terminaron los contratos. Por eso `freelance-guardrails` dice que rechazar es una acción del pipeline - la protección del JSS se hace eligiendo trabajos, no escribiendo el perfil. Si el usuario está arrancando, no tiene JSS, y ahí el portfolio y el nicho cargan todo el peso.
75
+
76
+ ## Proceso
77
+
78
+ 1. Leer el perfil maestro y el perfil actual de Upwork si existe.
79
+ 2. Correr el modo pedido (o `audit` si no se indicó ninguno).
80
+ 3. Proponer los textos como borradores en el chat, con **2 variantes de título** para elegir.
81
+ 4. Con approval, guardar el resultado en `01-Perfiles/Upwork.md` (derivado del maestro, no reemplazo). El usuario copia y pega en la plataforma - este skill nunca toca la cuenta.
82
+ 5. Registrar la fecha del último ajuste, para poder correlacionar cambios de perfil con cambios en la tasa de respuesta desde `/freelance-pipeline`.
83
+
84
+ ## Reglas
85
+
86
+ - Nunca inventar experiencia, clientes ni números.
87
+ - Nunca publicar trabajo bajo NDA ni de un cliente sin permiso.
88
+ - La tarifa nunca se deriva de la competencia; se deriva del piso.
89
+ - Sin guiones largos en los textos generados.
90
+
91
+ ## Handoff
92
+
93
+ Pedí aprobación (approval) antes de escribir `01-Perfiles/Upwork.md`. Perfil READY cuando título, overview y al menos dos portfolio items están completos. El siguiente paso es `/freelance-scan` para armar la primera cola de ofertas, o `/portfolio-site` si el usuario quiere un portfolio propio fuera de la plataforma.
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: freelance-proposal
3
+ description: "Write an Upwork proposal that survives the list view and price it above your net floor rate: hook in the first two lines, proof instead of adjectives, fixed vs hourly, risk buffer and platform fee. Reviewed by a fresh-context agent before spending Connects. Triggers: escribir propuesta upwork, cover letter upwork, cuanto cobrar por este proyecto, bid, cotizar proyecto freelance, propuesta freelance."
4
+ argument-hint: "[nota de aplicación | job-url]"
5
+ category: "career"
6
+ user-invocable: true
7
+ allowed-tools: Read, Glob, Grep, Write, Edit, WebFetch, Task
8
+ ---
9
+
10
+ # Upwork Proposal - Propuesta y bid
11
+
12
+ Escribís la propuesta y le ponés precio. Dos cosas en una skill porque en Upwork van juntas: el bid es parte de la propuesta y una no se defiende sin la otra.
13
+
14
+ Rutas relativas al career workspace (`--workspace`, env `CAREER_WORKSPACE`, o `./career-workspace/`).
15
+
16
+ Oferta indicada: `$ARGUMENTS`
17
+
18
+ ## Precondición: el screen
19
+
20
+ **No redactar una propuesta sin la nota de `/client-screen`.** Si no existe, pedirla y correr ese skill primero.
21
+
22
+ Si el screen dio riesgo alto o `NO POSTULARSE` y el usuario quiere postularse igual, se puede - **pero se le dice qué falló y se registra en la nota** (regla 1 de `freelance-guardrails`). Los Connects son plata: gastarlos a ciegas es distinto de gastarlos con los ojos abiertos.
23
+
24
+ ## Inputs
25
+
26
+ - **El job post completo** y la nota de `03-Aplicaciones/` con el screen.
27
+ - **El perfil base** de `01-Perfiles/` - de ahí salen la prueba y el **piso de tarifa neto**.
28
+ - **La estimación** de `/estimate` si el alcance es grande.
29
+ - Trabajos previos comparables (`08-Contratos/` cerrados) para calibrar precio contra realidad, no contra deseo.
30
+
31
+ ## Parte 1 - La propuesta
32
+
33
+ ### Cómo la lee el cliente
34
+
35
+ Tiene 30 propuestas en una lista y ve **las dos primeras líneas de cada una**. Todo lo que no esté ahí, no existe todavía. Esa es la única restricción de formato que importa.
36
+
37
+ ### Estructura
38
+
39
+ 1. **Dos primeras líneas (el 80% del trabajo).** Algo que solo se sabe habiendo leído *este* post: el problema real detrás del pedido, una observación técnica concreta, o el resultado que podés traer. Si la apertura serviría para cualquier otro trabajo del rubro, está muerta.
40
+ 2. **Prueba, 2-3 oraciones.** Un proyecto comparable con número o link. No adjetivos: "expert in React" es ruido, "migré un checkout de 40k usuarios a React, bajé el tiempo de carga 60%" es prueba.
41
+ 3. **Cómo lo harías, 3-4 puntos.** Suficiente para mostrar que ya lo pensaste, no tanto que regales la consultoría.
42
+ 4. **El precio, justificado.** Ver Parte 2. Un número suelto invita a regatear.
43
+ 5. **Una pregunta concreta** sobre el proyecto. Sin pregunta no arranca conversación, y demuestra que leíste el post.
44
+
45
+ **150-300 palabras.** Arriba de 300 casi nunca se lee entera.
46
+
47
+ ### Requisitos explícitos del post
48
+
49
+ Muchos posts piden algo puntual: empezar con una palabra clave, responder una pregunta específica, mandar un link. **Buscalo siempre y cumplilo.** Es descarte automático y es el error más barato de evitar.
50
+
51
+ ### Anti-patrones (cortar siempre)
52
+
53
+ - "I have X years of experience in..." como apertura. El cliente no contrata experiencia, contrata la solución de un problema que ya tiene.
54
+ - "Dear Sir/Madam", "I hope this message finds you well".
55
+ - Recitar el perfil en prosa.
56
+ - Adjetivos sin evidencia: passionate, detail-oriented, fast learner.
57
+ - Frases que delatan plantilla o generación automática. Los clientes de Upwork las reconocen al instante y es la razón número uno de descarte.
58
+ - Prometer plazos que no podés cumplir para ganar el contrato.
59
+
60
+ ## Parte 2 - El bid
61
+
62
+ ### El piso es neto
63
+
64
+ El piso de tarifa de `01-Perfiles/` es **lo que te queda**, no lo que facturás. Upwork se queda una comisión, así que el bid tiene que cubrirla: con un piso de 40 USD/h y 10% de comisión, el bid mínimo es ~45.
65
+
66
+ **Si el bid calculado queda abajo del piso, decilo fuerte y no lo maquilles.** El trabajo correcto ahí es no postularse o cotizar lo que corresponde y perder la oferta - no bajar el piso. Bajar la tarifa es la palanca más fácil de ejecutar y la más difícil de revertir: baja el techo de todos los contratos siguientes.
67
+
68
+ ### Fijo u hora
69
+
70
+ | Elegí fijo cuando | Elegí hora cuando |
71
+ |---|---|
72
+ | El alcance está escrito y acotado | El alcance es exploratorio o va a cambiar |
73
+ | Podés estimar con confianza media o alta | Hay dependencias del cliente que no controlás |
74
+ | Querés capturar valor arriba de tus horas | El cliente pide "iremos viendo" |
75
+
76
+ Alcance difuso + precio fijo es la combinación que más plata pierde. Si el screen marcó alcance difuso y el cliente insiste en fijo, el precio tiene que incluir el riesgo o el trabajo correcto es no tomarlo.
77
+
78
+ ### El cálculo
79
+
80
+ 1. Esfuerzo base de `/estimate`, en horas.
81
+ 2. **Buffer de riesgo** según lo que dijo el screen: alcance claro y cliente con historial, +15%; alcance difuso o cliente nuevo, +40% o más. El buffer no es grasa, es el precio de la incertidumbre que el cliente te transfiere.
82
+ 3. Horas no facturables del proyecto: reuniones, revisiones, ida y vuelta.
83
+ 4. Multiplicar por la tarifa objetivo (no por el piso: el piso es el mínimo, no la meta).
84
+ 5. Sumar la comisión de la plataforma.
85
+ 6. Comparar contra el presupuesto del cliente. Si no cierra, es dato para la decisión, no para bajar el número.
86
+
87
+ Mostrar el desglose al usuario **antes** de escribirlo en la propuesta. Al cliente se le muestra el total y su razón, no la planilla.
88
+
89
+ ## Proceso
90
+
91
+ 1. Leer el post, el screen y el perfil. Verificar que el screen exista.
92
+ 2. Calcular el bid (Parte 2) y mostrar el desglose. Si queda abajo del piso, avisar y parar a decidir.
93
+ 3. Draftear la propuesta (Parte 1). Después cortar sin piedad todo lo genérico.
94
+ 4. **Review con `proposal-reviewer`** (contexto fresco): pasale inline el post, el borrador y el bid. Aplicar sus reemplazos exactos e incorporar lo que corresponda. Una sola ronda. El que escribió leyó el post entero y ya está convencido de su propio gancho; el cliente ve dos líneas.
95
+ 5. Con approval, guardar en la nota de `03-Aplicaciones/`, sección `## Propuesta`, con el bid y su justificación en `## Cotización`.
96
+ 6. Si el usuario quiere ensayar la llamada, ofrecer `discovery-call`.
97
+
98
+ ## Idioma
99
+
100
+ En el idioma del post. Si el post está en inglés, la propuesta va en inglés aunque la charla sea en español.
101
+
102
+ ## Reglas
103
+
104
+ - Nunca inventar experiencia, clientes, plazos ni métricas.
105
+ - Nunca bidear abajo del piso neto sin decirlo explícitamente.
106
+ - Nunca enviar la propuesta: se muestra y el usuario copia y manda (regla 4 de `freelance-guardrails`).
107
+ - Nunca marcar `propuesta_enviada`: eso lo hace `/job-outcome` después del envío.
108
+ - Sin guiones largos en el texto de la propuesta ni de las notas.
109
+
110
+ ## Handoff
111
+
112
+ Pedí aprobación (approval) antes de escribir la propuesta en la nota. Propuesta READY cuando pasó el review y el bid está sobre el piso. El siguiente paso es enviarla a mano y correr `/job-outcome` para registrar el envío y el gasto de Connects; si el cliente responde, `discovery-call` para ensayar y después `/contract-and-proposal-writer` para el SOW.