empuno 1.0.1 → 1.1.1

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 (3) hide show
  1. package/README.md +18 -0
  2. package/empuno.mjs +279 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -51,6 +51,22 @@ komutu CLI güncellemesi olmadan görünür.
51
51
  - `--json '{…}'` / `--json @govde.json`, `--dry-run`, `--output yol`,
52
52
  `--profile ad` (birden çok firma).
53
53
 
54
+ ## Talepler (empuno ekibine)
55
+
56
+ Ajanınız yapamadığı bir işlemi, bir hatayı ya da geliştirme önerisini empuno
57
+ ekibine talep olarak iletir; yazışma çok mesajlıdır, ekip yanıtlar ve kapatır.
58
+
59
+ ```bash
60
+ empuno tickets open "Toplu izin onayı" --kind feature --body "Bekleyen izinleri tek komutla onaylamak istiyoruz." --command leave-requests.approve
61
+ empuno tickets --unread # ekibin yanıtladıkları
62
+ empuno tickets show TK-7Q3M9X # yazışmayı oku
63
+ empuno tickets reply TK-7Q3M9X "Her ay sonu gerekiyor."
64
+ empuno tickets close TK-7Q3M9X --note "Çözüldü"
65
+ ```
66
+
67
+ Türler: `feature`, `bug`, `criticism`, `question`, `missing_command`, `docs`.
68
+ Gövde için `--body @not.md` ya da `--body -` (stdin) kullanılabilir.
69
+
54
70
  ## Çıktı sözleşmesi
55
71
 
56
72
  - stdout **her zaman JSON** (TTY'de girintili).
@@ -66,6 +82,8 @@ empuno İK sistemine `empuno` CLI ile erişimin var. Önce `empuno commands <ana
66
82
  ile komutu bul, `empuno describe <komut>` ile parametrelerini öğren, sonra çalıştır.
67
83
  422 → `errors` alanlarını düzeltip tekrar dene; 403 → yetki yetmiyor, kullanıcıya
68
84
  `required_permission`ı söyle. Geri alınamaz işlemlerden önce kullanıcıdan onay al.
85
+ Yapamadığın işlem / hata / öneri için: empuno tickets open "<başlık>" --kind missing_command|bug|feature --body "…";
86
+ yanıtları `empuno tickets --unread` ile kontrol et.
69
87
  ```
70
88
 
71
89
  ## Güvenlik
package/empuno.mjs CHANGED
@@ -21,7 +21,7 @@ import os from 'node:os';
21
21
  import path from 'node:path';
22
22
  import crypto from 'node:crypto';
23
23
 
24
- const VERSION = '1.0.1'; // EMPUNO_CLI_VERSION — config/agent_api.php cli_version + package.json ile aynı tutulur
24
+ const VERSION = '1.1.1'; // EMPUNO_CLI_VERSION — config/agent_api.php cli_version + package.json ile aynı tutulur
25
25
  const NPM_PACKAGE = 'empuno';
26
26
  const DEFAULT_URL = 'https://empuno.com';
27
27
  const CATALOG_TTL_MS = 10 * 60 * 1000;
@@ -89,12 +89,13 @@ function resolveAuth(flags) {
89
89
  const profile = cfg.profiles?.[profileName] || {};
90
90
  const key = flags.key || process.env.EMPUNO_API_KEY || profile.key || null;
91
91
  const url = (flags.url || process.env.EMPUNO_URL || profile.url || DEFAULT_URL).replace(/\/+$/, '');
92
- return { key, url, profileName };
92
+ const staffToken = flags.token || process.env.EMPUNO_STAFF_TOKEN || profile.staff_token || null;
93
+ return { key, url, profileName, staffToken };
93
94
  }
94
95
 
95
96
  function requireKey(auth) {
96
97
  if (!auth.key) {
97
- throw new UsageError('API anahtarı yok. `empuno login` çalıştırın ya da EMPUNO_API_KEY ortam değişkenini ayarlayın. Anahtar: empuno paneli → Firma Ayarları → API → "Ajan" türünde anahtar.');
98
+ throw new UsageError('API anahtarı yok. `empuno login` çalıştırın ya da EMPUNO_API_KEY ortam değişkenini ayarlayın. Anahtar: empuno paneli → Firma Ayarları → API & Ajan → "Ajan" türünde anahtar.');
98
99
  }
99
100
  if (!auth.key.startsWith('emp_live_')) {
100
101
  throw new UsageError('Geçersiz anahtar biçimi (emp_live_ ile başlamalı).');
@@ -103,14 +104,15 @@ function requireKey(auth) {
103
104
 
104
105
  // ─── argüman ayrıştırma ────────────────────────────────────────────────────
105
106
 
106
- const GLOBAL_FLAGS = new Set(['key', 'url', 'profile', 'pretty', 'compact', 'text', 'json-out', 'dry-run', 'output', 'refresh', 'all', 'full', 'module', 'writes', 'reads', 'help', 'key-stdin', 'query', 'json', 'file', 'quiet']);
107
+ const GLOBAL_FLAGS = new Set(['key', 'token', 'url', 'profile', 'pretty', 'compact', 'text', 'json-out', 'dry-run', 'output', 'refresh', 'all', 'full', 'module', 'writes', 'reads', 'help', 'key-stdin', 'query', 'json', 'file', 'quiet']);
107
108
  const BOOLEAN_FLAGS = new Set(['pretty', 'compact', 'text', 'json-out', 'dry-run', 'refresh', 'all', 'full', 'writes', 'reads', 'help', 'key-stdin', 'quiet']);
108
109
 
109
110
  /**
110
111
  * argv → { positional: [], flags: {global}, fields: [[name, rawValue]] }.
111
112
  * `--ad değer`, `--ad=değer`, tekrar eden bayrak → dizi, `--` sonrası hep positional.
112
113
  */
113
- function parseArgs(argv) {
114
+ function parseArgs(argv, extraBooleans = []) {
115
+ const extraBool = new Set(extraBooleans);
114
116
  const positional = [];
115
117
  const flags = { query: [], file: [] };
116
118
  const fields = [];
@@ -134,7 +136,7 @@ function parseArgs(argv) {
134
136
  name = name.slice(0, eq);
135
137
  }
136
138
 
137
- if (GLOBAL_FLAGS.has(name) && BOOLEAN_FLAGS.has(name)) {
139
+ if ((GLOBAL_FLAGS.has(name) && BOOLEAN_FLAGS.has(name)) || extraBool.has(name)) {
138
140
  flags[name] = value === undefined ? true : !['0', 'false', 'no'].includes(value);
139
141
  i++;
140
142
  continue;
@@ -380,7 +382,7 @@ async function findCommand(auth, name, flags) {
380
382
  if (cmd) return cmd;
381
383
 
382
384
  if (catalog.access === 'read') {
383
- const err = new UsageError('bu anahtar salt-okuma ("read") türünde: panel komutlarını çalıştıramaz, yalnız `empuno api …`. Firma Ayarları → API sekmesinden "Ajan" anahtarı oluşturun.');
385
+ const err = new UsageError('bu anahtar salt-okuma ("read") türünde: panel komutlarını çalıştıramaz, yalnız `empuno api …`. Firma Ayarları → API & Ajan sekmesinden "Ajan" anahtarı oluşturun.');
384
386
  err.payload = { ok: false, error: 'read_only_key' };
385
387
  throw err;
386
388
  }
@@ -392,8 +394,8 @@ async function findCommand(auth, name, flags) {
392
394
 
393
395
  if (cmd) {
394
396
  const need = cmd.requires_company_admin ? ['company_admin'] : cmd.permissions;
395
- const err = new UsageError(`"${name}" bu anahtarın kapsamında değil. Gereken yetki: ${need.join(', ') || '—'}. Anahtar kapsamını panelden genişletin (Firma Ayarları → API).`);
396
- err.payload = { ok: false, error: 'scope_required', command: name, required: need };
397
+ const err = new UsageError(`"${name}" bu anahtarın kapsamında değil. Gereken yetki: ${need.join(', ') || '—'}. Anahtar kapsamını panelden genişletin (Firma Ayarları → API & Ajan).`);
398
+ err.payload = { ok: false, error: 'scope_required', command: name, required: need, hint: 'Kapsamı anahtar sahibi panelden genişletir; kullanıcıya bildirin.' };
397
399
  throw err;
398
400
  }
399
401
 
@@ -402,7 +404,13 @@ async function findCommand(auth, name, flags) {
402
404
  .filter((c) => c.includes(name.split('.')[0]) || name.includes(c.split('.')[0]))
403
405
  .slice(0, 8);
404
406
  const err = new UsageError(`bilinmeyen komut "${name}".${suggestions.length ? ` Şunlardan biri mi: ${suggestions.join(', ')}` : ' `empuno commands` ile listeleyin.'}`);
405
- err.payload = { ok: false, error: 'unknown_command', command: name, suggestions };
407
+ err.payload = {
408
+ ok: false,
409
+ error: 'unknown_command',
410
+ command: name,
411
+ suggestions,
412
+ hint: `Gereken işlem yoksa talep açın: empuno tickets open "<başlık>" --kind missing_command --command ${name} --body "<ne yapmak istediğiniz>"`,
413
+ };
406
414
  throw err;
407
415
  }
408
416
 
@@ -671,6 +679,244 @@ async function cmdUpdate(args) {
671
679
  return 0;
672
680
  }
673
681
 
682
+ // ─── talepler (ticket) ─────────────────────────────────────────────────────
683
+
684
+ const TICKET_KINDS = {
685
+ feature: 'Geliştirme isteği',
686
+ bug: 'Hata bildirimi',
687
+ criticism: 'Eleştiri / geri bildirim',
688
+ question: 'Soru',
689
+ missing_command: 'Eksik komut / işlem',
690
+ docs: 'Dokümantasyon',
691
+ };
692
+
693
+ /** Alan bayraklarını nesneye çevirir (son değer kazanır). */
694
+ function fieldMap(fields) {
695
+ const out = {};
696
+ for (const [k, v] of fields) out[k] = v;
697
+ return out;
698
+ }
699
+
700
+ /** "@dosya.md" → dosya içeriği; "-" → stdin; diğerleri olduğu gibi. */
701
+ function readText(value) {
702
+ if (value === undefined || value === null || value === true) return undefined;
703
+ const text = String(value);
704
+ if (text === '-') return fs.readFileSync(0, 'utf8');
705
+ if (text.startsWith('@') && text.length > 1) {
706
+ const file = text.slice(1);
707
+ if (!fs.existsSync(file)) throw new UsageError(`dosya bulunamadı: ${file}`);
708
+ return fs.readFileSync(file, 'utf8');
709
+ }
710
+ return text;
711
+ }
712
+
713
+ function ticketNumber(value) {
714
+ if (!value) throw new UsageError('talep numarası gerekli (ör. TK-7Q3M9X)');
715
+ const n = String(value).toUpperCase();
716
+ return n.startsWith('TK-') ? n : `TK-${n}`;
717
+ }
718
+
719
+ function printTicketList(result, perspective) {
720
+ if (!wantsText() || !Array.isArray(result?.data)) {
721
+ print(result);
722
+ return;
723
+ }
724
+ for (const t of result.data) {
725
+ const dot = t.unread ? '●' : ' ';
726
+ const who = perspective === 'staff' && t.company ? ` · ${t.company.name}` : '';
727
+ process.stdout.write(`${dot} ${t.number} ${t.status.padEnd(8)} ${t.kind.padEnd(15)} ${t.title}${who}\n`);
728
+ }
729
+ const total = result.meta?.total ?? result.data.length;
730
+ process.stdout.write(`\n${total} talep. Ayrıntı: empuno ${perspective === 'staff' ? 'support' : 'tickets'} show <numara>\n`);
731
+ }
732
+
733
+ function printTicket(result) {
734
+ const t = result?.data;
735
+ if (!wantsText() || !t?.messages) {
736
+ print(result);
737
+ return;
738
+ }
739
+ process.stdout.write(`${t.number} · ${t.title}\n${t.kind_label} · öncelik ${t.priority} · ${t.status}${t.company ? ` · ${t.company.name}` : ''}\n`);
740
+ for (const m of t.messages) {
741
+ if (m.author_type === 'system') {
742
+ process.stdout.write(`\n — ${m.body} (${m.created_at})\n`);
743
+ continue;
744
+ }
745
+ process.stdout.write(`\n[${m.author_type === 'staff' ? 'empuno' : 'ajan'}] ${m.author} — ${m.created_at}\n${m.body}\n`);
746
+ }
747
+ }
748
+
749
+ async function cmdTickets(args) {
750
+ const { flags, fields, positional } = args;
751
+ const opts = fieldMap(fields);
752
+ const [action = 'list', ...rest] = positional;
753
+ const base = '/api/company/v1/tickets';
754
+
755
+ if (action === 'kinds') {
756
+ print({ kinds: TICKET_KINDS, priorities: ['low', 'normal', 'high'] });
757
+ return 0;
758
+ }
759
+
760
+ const auth = resolveAuth(flags);
761
+ requireKey(auth);
762
+
763
+ const send = async (method, urlPath, body = null, query = {}) => {
764
+ const response = await request(auth, method, urlPath, { body, query, dryRun: flags['dry-run'] });
765
+ return response;
766
+ };
767
+
768
+ switch (action) {
769
+ case 'list':
770
+ case 'ls': {
771
+ const query = {};
772
+ if (opts.status) query.status = opts.status;
773
+ if (flags.mine) query.mine = 1;
774
+ if (flags.unread) query.unread = 1;
775
+ if (opts.page) query.page = opts.page;
776
+ if (opts.per_page) query.per_page = opts.per_page;
777
+ const r = await send('GET', base, null, query);
778
+ if (r.dryRun) return print(r.result), 0;
779
+ printTicketList(r.result, 'agent');
780
+ return r.ok ? 0 : 1;
781
+ }
782
+ case 'open':
783
+ case 'new': {
784
+ const title = rest[0] ?? opts.title;
785
+ const body = readText(opts.body ?? rest[1]);
786
+ const kind = opts.kind ?? 'feature';
787
+ if (!title || !body) {
788
+ throw new UsageError('kullanım: empuno tickets open "<başlık>" --kind feature|bug|criticism|question|missing_command|docs --body "<açıklama>"|@dosya.md');
789
+ }
790
+ let context = opts.context ? readJsonArg(String(opts.context)) : {};
791
+ if (opts.command) context = { ...context, command: String(opts.command) };
792
+ const r = await send('POST', base, { title: String(title), kind, body, priority: opts.priority, context: Object.keys(context).length ? context : undefined });
793
+ if (r.dryRun) return print(r.result), 0;
794
+ print(r.result);
795
+ return r.ok ? 0 : 1;
796
+ }
797
+ case 'show':
798
+ case 'get': {
799
+ const r = await send('GET', `${base}/${ticketNumber(rest[0])}`);
800
+ if (r.dryRun) return print(r.result), 0;
801
+ printTicket(r.result);
802
+ return r.ok ? 0 : 1;
803
+ }
804
+ case 'reply': {
805
+ const body = readText(opts.body ?? rest[1]);
806
+ if (!body) throw new UsageError('kullanım: empuno tickets reply <numara> "<mesaj>" (ya da --body @dosya.md)');
807
+ const r = await send('POST', `${base}/${ticketNumber(rest[0])}/messages`, { body });
808
+ print(r.result);
809
+ return r.ok || r.dryRun ? 0 : 1;
810
+ }
811
+ case 'close': {
812
+ const note = readText(opts.note ?? rest[1]);
813
+ const r = await send('POST', `${base}/${ticketNumber(rest[0])}/close`, note ? { note } : {});
814
+ print(r.result);
815
+ return r.ok || r.dryRun ? 0 : 1;
816
+ }
817
+ default:
818
+ throw new UsageError(`bilinmeyen tickets işlemi "${action}". Kullanım: empuno tickets [list|open|show|reply|close|kinds]`);
819
+ }
820
+ }
821
+
822
+ // ─── destek (yalnız empuno ekibinin ajanları — emp_staff_ jetonu) ────────────
823
+
824
+ function requireStaff(auth) {
825
+ if (!auth.staffToken) {
826
+ throw new UsageError('destek jetonu yok. `empuno support login` ya da EMPUNO_STAFF_TOKEN. Jeton: empuno root paneli → Ajan Talepleri → Destek ajanları.');
827
+ }
828
+ if (!auth.staffToken.startsWith('emp_staff_')) {
829
+ throw new UsageError('geçersiz destek jetonu biçimi (emp_staff_ ile başlamalı).');
830
+ }
831
+ return { ...auth, key: auth.staffToken };
832
+ }
833
+
834
+ async function cmdSupport(args) {
835
+ const { flags, fields, positional } = args;
836
+ const opts = fieldMap(fields);
837
+ const [action = 'tickets', ...rest] = positional;
838
+ const base = '/api/support/v1';
839
+
840
+ if (action === 'login') {
841
+ let token = flags.token || process.env.EMPUNO_STAFF_TOKEN || rest[0];
842
+ if (flags['token-stdin'] || (!token && !process.stdin.isTTY)) token = fs.readFileSync(0, 'utf8').trim();
843
+ else if (!token) token = await prompt('Destek jetonu (emp_staff_…): ');
844
+ const auth = requireStaff({ ...resolveAuth(flags), staffToken: token?.trim() });
845
+ const { ok, result } = await request(auth, 'GET', `${base}/me`);
846
+ if (!ok) {
847
+ print(result);
848
+ return 1;
849
+ }
850
+ const cfg = readConfig();
851
+ cfg.profiles = cfg.profiles || {};
852
+ const name = auth.profileName;
853
+ cfg.profiles[name] = { ...(cfg.profiles[name] || { url: auth.url }), staff_token: auth.key };
854
+ if (!cfg.default || !cfg.profiles[cfg.default]) cfg.default = name;
855
+ writeConfig(cfg);
856
+ print({ ok: true, profile: name, token: result.token, counts: result.counts });
857
+ return 0;
858
+ }
859
+
860
+ const auth = requireStaff(resolveAuth(flags));
861
+ const call = (method, urlPath, body = null, query = {}) => request(auth, method, urlPath, { body, query, dryRun: flags['dry-run'] });
862
+
863
+ switch (action) {
864
+ case 'whoami':
865
+ case 'me': {
866
+ const r = await call('GET', `${base}/me`);
867
+ print(r.result);
868
+ return r.ok || r.dryRun ? 0 : 1;
869
+ }
870
+ case 'tickets':
871
+ case 'list':
872
+ case 'ls': {
873
+ const query = {};
874
+ for (const k of ['status', 'kind', 'company', 'q', 'page', 'per_page']) if (opts[k] !== undefined) query[k] = opts[k];
875
+ if (flags.unread) query.unread = 1;
876
+ const r = await call('GET', `${base}/tickets`, null, query);
877
+ if (r.dryRun) return print(r.result), 0;
878
+ printTicketList(r.result, 'staff');
879
+ return r.ok ? 0 : 1;
880
+ }
881
+ case 'show':
882
+ case 'get': {
883
+ const r = await call('GET', `${base}/tickets/${ticketNumber(rest[0])}`);
884
+ if (r.dryRun) return print(r.result), 0;
885
+ printTicket(r.result);
886
+ return r.ok ? 0 : 1;
887
+ }
888
+ case 'reply': {
889
+ const body = readText(opts.body ?? rest[1]);
890
+ if (!body) throw new UsageError('kullanım: empuno support reply <numara> "<yanıt>" [--close]');
891
+ const r = await call('POST', `${base}/tickets/${ticketNumber(rest[0])}/messages`, { body, close: flags.close ? true : undefined });
892
+ print(r.result);
893
+ return r.ok || r.dryRun ? 0 : 1;
894
+ }
895
+ case 'close': {
896
+ const note = readText(opts.note ?? rest[1]);
897
+ const r = await call('POST', `${base}/tickets/${ticketNumber(rest[0])}/close`, note ? { note } : {});
898
+ print(r.result);
899
+ return r.ok || r.dryRun ? 0 : 1;
900
+ }
901
+ case 'reopen': {
902
+ const r = await call('POST', `${base}/tickets/${ticketNumber(rest[0])}/reopen`);
903
+ print(r.result);
904
+ return r.ok || r.dryRun ? 0 : 1;
905
+ }
906
+ case 'set': {
907
+ const body = {};
908
+ if (opts.kind) body.kind = opts.kind;
909
+ if (opts.priority) body.priority = opts.priority;
910
+ if (!Object.keys(body).length) throw new UsageError('kullanım: empuno support set <numara> [--kind bug] [--priority high]');
911
+ const r = await call('PATCH', `${base}/tickets/${ticketNumber(rest[0])}`, body);
912
+ print(r.result);
913
+ return r.ok || r.dryRun ? 0 : 1;
914
+ }
915
+ default:
916
+ throw new UsageError(`bilinmeyen support işlemi "${action}". Kullanım: empuno support [login|whoami|tickets|show|reply|close|reopen|set]`);
917
+ }
918
+ }
919
+
674
920
  const HELP = `empuno ${VERSION} — empuno İK sistemini komut satırından (ve ajanlardan) yönetin
675
921
 
676
922
  KURULUM
@@ -700,6 +946,18 @@ ALANLAR
700
946
  --alan @./dosya.pdf | --file alan=./dosya.pdf # multipart yükleme
701
947
  --query anahtar=değer # yazma isteğinde ek sorgu parametresi
702
948
 
949
+ TALEPLER (geliştirme isteği, hata, eleştiri, soru → empuno ekibi)
950
+ empuno tickets [--status active|open|answered|closed|all] [--unread] [--mine]
951
+ empuno tickets open "Toplu izin onayı" --kind feature --body "…"|@not.md [--priority high] [--command leave-requests.approve]
952
+ empuno tickets show TK-7Q3M9X # yazışmayı okur (okunmadı bayrağı düşer)
953
+ empuno tickets reply TK-7Q3M9X "Ek bilgi…"
954
+ empuno tickets close TK-7Q3M9X [--note "Çözüldü"]
955
+ empuno tickets kinds # feature, bug, criticism, question, missing_command, docs
956
+
957
+ EMPUNO EKİBİ (destek jetonu emp_staff_…, EMPUNO_STAFF_TOKEN)
958
+ empuno support login | whoami | tickets [--unread] [--status] [--kind] [--company]
959
+ empuno support show|reply|close|reopen <numara> ["mesaj"] [--close] · empuno support set <numara> --priority high
960
+
703
961
  GENEL BAYRAKLAR
704
962
  --profile ad --key emp_live_… --url https://empuno.com --dry-run --output yol
705
963
  --text | --json-out --pretty | --compact --refresh (katalog önbelleğini tazele)
@@ -714,7 +972,8 @@ BELGE: https://empuno.com/api-docs/agent
714
972
 
715
973
  async function main() {
716
974
  const argv = process.argv.slice(2);
717
- const args = parseArgs(argv);
975
+ const ticketish = ['tickets', 'ticket', 'support'].includes(argv.find((a) => !a.startsWith('--')) ?? '');
976
+ const args = parseArgs(argv, ticketish ? ['mine', 'unread', 'close', 'token-stdin'] : []);
718
977
  const [sub, ...rest] = args.positional;
719
978
  args.positional = rest;
720
979
 
@@ -723,6 +982,10 @@ async function main() {
723
982
  return 0;
724
983
  }
725
984
  if (args.flags.help) {
985
+ if (['tickets', 'ticket', 'support', 'login', 'commands', 'describe', 'call', 'api'].includes(sub)) {
986
+ process.stdout.write(HELP);
987
+ return 0;
988
+ }
726
989
  args.positional = [sub];
727
990
  return cmdDescribe(args);
728
991
  }
@@ -750,6 +1013,11 @@ async function main() {
750
1013
  return cmdApi(args);
751
1014
  case 'update':
752
1015
  return cmdUpdate(args);
1016
+ case 'tickets':
1017
+ case 'ticket':
1018
+ return cmdTickets(args);
1019
+ case 'support':
1020
+ return cmdSupport(args);
753
1021
  case 'run':
754
1022
  if (!args.positional[0]) throw new UsageError('kullanım: empuno run <komut> …');
755
1023
  return cmdRun({ ...args, positional: args.positional.slice(1) }, args.positional[0]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "empuno",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "empuno İK platformunun komut satırı: yönetim panelindeki tüm işlemleri (izin, bordro, vardiya, PDKS, zimmet, masraf, işe alım) yapay zekâ ajanlarına ve otomasyonlara açar.",
5
5
  "type": "module",
6
6
  "bin": {