agent-office 0.4.5 → 0.4.7

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.
@@ -43,6 +43,22 @@ async function fetchMessages(agentUrl, password, humanName, coworker) {
43
43
  async function markRead(agentUrl, password, id) {
44
44
  await apiFetch(agentUrl, password, `/messages/${id}/read`, { method: "POST" });
45
45
  }
46
+ async function fetchCronRequests(agentUrl, password, status) {
47
+ const params = status ? `?status=${encodeURIComponent(status)}` : "";
48
+ return apiFetch(agentUrl, password, `/cron-requests${params}`);
49
+ }
50
+ async function approveCronRequest(agentUrl, password, id, notes) {
51
+ await apiFetch(agentUrl, password, `/cron-requests/${id}/approve`, {
52
+ method: "POST",
53
+ body: JSON.stringify({ notes })
54
+ });
55
+ }
56
+ async function rejectCronRequest(agentUrl, password, id, notes) {
57
+ await apiFetch(agentUrl, password, `/cron-requests/${id}/reject`, {
58
+ method: "POST",
59
+ body: JSON.stringify({ notes })
60
+ });
61
+ }
46
62
  // ── HTML helpers ──────────────────────────────────────────────────────────────
47
63
  function escapeHtml(str) {
48
64
  return str
@@ -58,6 +74,15 @@ function formatTime(iso) {
58
74
  hour: "2-digit", minute: "2-digit",
59
75
  });
60
76
  }
77
+ function formatFullTime(iso) {
78
+ return new Date(iso).toLocaleString(undefined, {
79
+ year: "numeric",
80
+ month: "short",
81
+ day: "numeric",
82
+ hour: "2-digit",
83
+ minute: "2-digit",
84
+ });
85
+ }
61
86
  function renderMessage(msg, humanName, spacingClass) {
62
87
  const isMine = msg.from_name === humanName;
63
88
  const bubbleClass = isMine ? "bubble bubble-mine" : "bubble bubble-theirs";
@@ -227,6 +252,16 @@ function renderPage(coworker, coworkers, msgs, humanName) {
227
252
  .refresh-indicator.active { background: var(--green); }
228
253
 
229
254
  /* ── Reset button ── */
255
+ .header-link {
256
+ color: var(--text-dim);
257
+ text-decoration: none;
258
+ font-size: 18px;
259
+ margin-right: 8px;
260
+ transition: color 0.15s;
261
+ flex-shrink: 0;
262
+ }
263
+ .header-link:hover { color: var(--accent); }
264
+
230
265
  .reset-btn {
231
266
  background: none;
232
267
  border: 1px solid var(--border);
@@ -475,6 +510,7 @@ function renderPage(coworker, coworkers, msgs, humanName) {
475
510
  hx-trigger="load, every 5s"
476
511
  hx-swap="innerHTML"></div>
477
512
  </div>
513
+ <a href="/cron-requests" class="header-link" title="Manage cron job requests">⚙️</a>
478
514
  <button class="reset-btn"
479
515
  hx-post="/reset?coworker=${encodeURIComponent(selected)}"
480
516
  hx-target="#reset-status"
@@ -647,6 +683,501 @@ function renderPage(coworker, coworkers, msgs, humanName) {
647
683
  </body>
648
684
  </html>`;
649
685
  }
686
+ function renderCronRequestsPage(requests) {
687
+ const pendingCount = requests.filter(r => r.status === 'pending').length;
688
+ const approvedCount = requests.filter(r => r.status === 'approved').length;
689
+ const rejectedCount = requests.filter(r => r.status === 'rejected').length;
690
+ const requestsHtml = requests.length === 0
691
+ ? `<div class="empty-state">No cron requests found.</div>`
692
+ : requests.map(r => {
693
+ const statusColor = r.status === 'pending' ? 'var(--accent)' :
694
+ r.status === 'approved' ? 'var(--green)' : 'var(--red)';
695
+ const actionButtons = r.status === 'pending'
696
+ ? `<div class="request-actions">
697
+ <button class="action-btn approve-btn"
698
+ hx-post="/approve-request?id=${r.id}"
699
+ hx-confirm="Approve this cron job request?"
700
+ hx-target="#action-status"
701
+ hx-swap="innerHTML"
702
+ title="Approve request">✓</button>
703
+ <button class="action-btn reject-btn"
704
+ hx-post="/reject-request?id=${r.id}"
705
+ hx-confirm="Reject this cron job request?"
706
+ hx-target="#action-status"
707
+ hx-swap="innerHTML"
708
+ title="Reject request">✗</button>
709
+ </div>`
710
+ : '';
711
+ return `<div class="request-card" style="border-left: 4px solid ${statusColor}">
712
+ <div class="request-header">
713
+ <div class="request-title">
714
+ <strong>${escapeHtml(r.name)}</strong>
715
+ <span class="request-status" style="color: ${statusColor}">${r.status.toUpperCase()}</span>
716
+ </div>
717
+ <div class="request-meta">
718
+ <span class="request-coworker">👤 ${escapeHtml(r.session_name)}</span>
719
+ <span class="request-time">🕒 ${formatFullTime(r.requested_at)}</span>
720
+ </div>
721
+ </div>
722
+ <div class="request-details">
723
+ <div class="request-schedule">
724
+ <strong>Schedule:</strong> <code>${escapeHtml(r.schedule)}</code>
725
+ ${r.timezone ? `<span class="timezone">(TZ: ${escapeHtml(r.timezone)})</span>` : ''}
726
+ </div>
727
+ <div class="request-message">
728
+ <strong>Message:</strong>
729
+ <div class="message-content markdown-body" data-markdown>${escapeHtml(r.message)}</div>
730
+ </div>
731
+ ${r.reviewed_at && r.reviewed_by ? `
732
+ <div class="request-review">
733
+ <strong>Reviewed by ${escapeHtml(r.reviewed_by)} on ${formatFullTime(r.reviewed_at)}</strong>
734
+ ${r.reviewer_notes ? `<div class="review-notes">${escapeHtml(r.reviewer_notes)}</div>` : ''}
735
+ </div>
736
+ ` : ''}
737
+ </div>
738
+ ${actionButtons}
739
+ </div>`;
740
+ }).join('\n');
741
+ return `<!DOCTYPE html>
742
+ <html lang="en">
743
+ <head>
744
+ <meta charset="UTF-8">
745
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
746
+ <title>Cron Requests — agent-office</title>
747
+ <script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js"></script>
748
+ <script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
749
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.5.1/github-markdown-light.min.css" media="(prefers-color-scheme: light)">
750
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.5.1/github-markdown-dark.min.css" media="(prefers-color-scheme: dark)">
751
+ <style>
752
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
753
+
754
+ :root {
755
+ --bg: #0f1117;
756
+ --surface: #1a1d27;
757
+ --surface2: #22263a;
758
+ --border: #2e3248;
759
+ --accent: #6c8eff;
760
+ --accent-dim: #3d52a0;
761
+ --text: #e2e8f0;
762
+ --text-dim: #8892a4;
763
+ --green: #6bffb8;
764
+ --red: #ff6b6b;
765
+ --radius: 12px;
766
+ --radius-sm: 6px;
767
+ --header-h: 56px;
768
+ font-size: 16px;
769
+ }
770
+
771
+ html, body {
772
+ height: 100%;
773
+ background: var(--bg);
774
+ color: var(--text);
775
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
776
+ overflow: hidden;
777
+ }
778
+
779
+ .app {
780
+ display: flex;
781
+ flex-direction: column;
782
+ height: 100dvh;
783
+ }
784
+
785
+ /* Header */
786
+ .header {
787
+ flex-shrink: 0;
788
+ height: var(--header-h);
789
+ background: var(--surface);
790
+ border-bottom: 1px solid var(--border);
791
+ display: flex;
792
+ align-items: center;
793
+ padding: 0 16px;
794
+ gap: 12px;
795
+ }
796
+
797
+ .header-title {
798
+ font-weight: 600;
799
+ font-size: 18px;
800
+ color: var(--accent);
801
+ }
802
+
803
+ .header-stats {
804
+ margin-left: auto;
805
+ display: flex;
806
+ gap: 16px;
807
+ font-size: 14px;
808
+ }
809
+
810
+ .stat-item {
811
+ display: flex;
812
+ align-items: center;
813
+ gap: 4px;
814
+ }
815
+
816
+ .stat-count {
817
+ font-weight: 600;
818
+ }
819
+
820
+ .back-link {
821
+ color: var(--text-dim);
822
+ text-decoration: none;
823
+ font-size: 16px;
824
+ transition: color 0.15s;
825
+ }
826
+ .back-link:hover { color: var(--accent); }
827
+
828
+ /* Content */
829
+ .content {
830
+ flex: 1;
831
+ overflow-y: auto;
832
+ padding: 20px;
833
+ display: flex;
834
+ flex-direction: column;
835
+ gap: 16px;
836
+ }
837
+
838
+ .filter-tabs {
839
+ display: flex;
840
+ gap: 8px;
841
+ margin-bottom: 16px;
842
+ }
843
+
844
+ .filter-tab {
845
+ padding: 8px 16px;
846
+ background: var(--surface2);
847
+ border: 1px solid var(--border);
848
+ border-radius: var(--radius-sm);
849
+ color: var(--text-dim);
850
+ text-decoration: none;
851
+ font-size: 14px;
852
+ transition: all 0.15s;
853
+ }
854
+
855
+ .filter-tab:hover {
856
+ border-color: var(--accent-dim);
857
+ color: var(--accent);
858
+ }
859
+
860
+ .filter-tab.active {
861
+ background: var(--accent);
862
+ color: #fff;
863
+ border-color: var(--accent);
864
+ }
865
+
866
+ .empty-state {
867
+ text-align: center;
868
+ color: var(--text-dim);
869
+ font-size: 16px;
870
+ padding: 48px;
871
+ }
872
+
873
+ /* Request Cards */
874
+ .request-card {
875
+ background: var(--surface);
876
+ border: 1px solid var(--border);
877
+ border-radius: var(--radius);
878
+ padding: 16px;
879
+ margin-bottom: 12px;
880
+ }
881
+
882
+ .request-header {
883
+ display: flex;
884
+ justify-content: space-between;
885
+ align-items: flex-start;
886
+ margin-bottom: 12px;
887
+ }
888
+
889
+ .request-title {
890
+ display: flex;
891
+ align-items: center;
892
+ gap: 8px;
893
+ font-size: 16px;
894
+ }
895
+
896
+ .request-status {
897
+ font-size: 12px;
898
+ font-weight: 600;
899
+ padding: 2px 8px;
900
+ border-radius: var(--radius-sm);
901
+ background: rgba(255, 255, 255, 0.1);
902
+ }
903
+
904
+ .request-meta {
905
+ display: flex;
906
+ flex-direction: column;
907
+ align-items: flex-end;
908
+ gap: 2px;
909
+ font-size: 12px;
910
+ color: var(--text-dim);
911
+ }
912
+
913
+ .request-details {
914
+ display: flex;
915
+ flex-direction: column;
916
+ gap: 8px;
917
+ font-size: 14px;
918
+ }
919
+
920
+ .request-schedule {
921
+ display: flex;
922
+ align-items: center;
923
+ gap: 8px;
924
+ flex-wrap: wrap;
925
+ }
926
+
927
+ .request-schedule code {
928
+ background: var(--surface2);
929
+ padding: 2px 6px;
930
+ border-radius: var(--radius-sm);
931
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
932
+ font-size: 13px;
933
+ }
934
+
935
+ .timezone {
936
+ color: var(--text-dim);
937
+ font-size: 12px;
938
+ }
939
+
940
+ .request-message {
941
+ margin-top: 4px;
942
+ }
943
+
944
+ .message-content {
945
+ background: var(--surface2);
946
+ border: 1px solid var(--border);
947
+ border-radius: var(--radius-sm);
948
+ padding: 8px 12px;
949
+ margin-top: 4px;
950
+ font-size: 13px;
951
+ line-height: 1.4;
952
+ white-space: pre-wrap;
953
+ word-break: break-word;
954
+ }
955
+
956
+ .request-review {
957
+ margin-top: 8px;
958
+ padding-top: 8px;
959
+ border-top: 1px solid var(--border);
960
+ font-size: 13px;
961
+ color: var(--text-dim);
962
+ }
963
+
964
+ .review-notes {
965
+ margin-top: 4px;
966
+ background: rgba(255, 255, 255, 0.05);
967
+ padding: 6px 10px;
968
+ border-radius: var(--radius-sm);
969
+ border-left: 3px solid var(--accent);
970
+ }
971
+
972
+ .request-actions {
973
+ display: flex;
974
+ gap: 8px;
975
+ margin-top: 12px;
976
+ padding-top: 12px;
977
+ border-top: 1px solid var(--border);
978
+ }
979
+
980
+ .action-btn {
981
+ padding: 6px 12px;
982
+ border: 1px solid var(--border);
983
+ border-radius: var(--radius-sm);
984
+ cursor: pointer;
985
+ font-size: 14px;
986
+ font-weight: 600;
987
+ transition: all 0.15s;
988
+ display: flex;
989
+ align-items: center;
990
+ justify-content: center;
991
+ min-width: 32px;
992
+ }
993
+
994
+ .approve-btn {
995
+ background: rgba(107, 255, 184, 0.1);
996
+ border-color: var(--green);
997
+ color: var(--green);
998
+ }
999
+
1000
+ .approve-btn:hover {
1001
+ background: var(--green);
1002
+ color: #fff;
1003
+ }
1004
+
1005
+ .reject-btn {
1006
+ background: rgba(255, 107, 107, 0.1);
1007
+ border-color: var(--red);
1008
+ color: var(--red);
1009
+ }
1010
+
1011
+ .reject-btn:hover {
1012
+ background: var(--red);
1013
+ color: #fff;
1014
+ }
1015
+
1016
+ .action-btn.htmx-request {
1017
+ opacity: 0.6;
1018
+ pointer-events: none;
1019
+ }
1020
+
1021
+ /* Status messages */
1022
+ #action-status {
1023
+ position: fixed;
1024
+ bottom: 20px;
1025
+ left: 50%;
1026
+ transform: translateX(-50%);
1027
+ background: var(--surface);
1028
+ border: 1px solid var(--border);
1029
+ border-radius: var(--radius);
1030
+ padding: 12px 20px;
1031
+ font-size: 14px;
1032
+ pointer-events: none;
1033
+ opacity: 0;
1034
+ transition: opacity 0.3s;
1035
+ z-index: 1000;
1036
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
1037
+ }
1038
+
1039
+ #action-status.visible {
1040
+ opacity: 1;
1041
+ }
1042
+
1043
+ #action-status.success {
1044
+ border-color: var(--green);
1045
+ color: var(--green);
1046
+ }
1047
+
1048
+ #action-status.error {
1049
+ border-color: var(--red);
1050
+ color: var(--red);
1051
+ }
1052
+
1053
+ /* Markdown styling */
1054
+ .markdown-body {
1055
+ background: transparent;
1056
+ color: inherit;
1057
+ }
1058
+
1059
+ .markdown-body p { margin: 0 0 8px 0; }
1060
+ .markdown-body p:last-child { margin-bottom: 0; }
1061
+ .markdown-body pre {
1062
+ background: rgba(0,0,0,0.3);
1063
+ border-radius: 6px;
1064
+ padding: 8px 12px;
1065
+ overflow-x: auto;
1066
+ margin: 8px 0;
1067
+ }
1068
+ .markdown-body code {
1069
+ background: rgba(0,0,0,0.2);
1070
+ padding: 2px 5px;
1071
+ border-radius: 3px;
1072
+ font-size: 12px;
1073
+ }
1074
+ .markdown-body pre code {
1075
+ background: transparent;
1076
+ padding: 0;
1077
+ }
1078
+ </style>
1079
+ </head>
1080
+ <body>
1081
+ <div class="app">
1082
+ <div class="header">
1083
+ <a href="/" class="back-link" title="Back to chat">←</a>
1084
+ <div class="header-title">Cron Requests</div>
1085
+ <div class="header-stats">
1086
+ <div class="stat-item">
1087
+ <span>⏳</span>
1088
+ <span class="stat-count">${pendingCount}</span>
1089
+ <span>pending</span>
1090
+ </div>
1091
+ <div class="stat-item">
1092
+ <span>✅</span>
1093
+ <span class="stat-count">${approvedCount}</span>
1094
+ <span>approved</span>
1095
+ </div>
1096
+ <div class="stat-item">
1097
+ <span>❌</span>
1098
+ <span class="stat-count">${rejectedCount}</span>
1099
+ <span>rejected</span>
1100
+ </div>
1101
+ </div>
1102
+ </div>
1103
+
1104
+ <div class="content">
1105
+ <div class="filter-tabs">
1106
+ <a href="/cron-requests" class="filter-tab">All</a>
1107
+ <a href="/cron-requests?status=pending" class="filter-tab">Pending</a>
1108
+ <a href="/cron-requests?status=approved" class="filter-tab">Approved</a>
1109
+ <a href="/cron-requests?status=rejected" class="filter-tab">Rejected</a>
1110
+ </div>
1111
+
1112
+ ${requestsHtml}
1113
+ </div>
1114
+
1115
+ <div id="action-status"></div>
1116
+ </div>
1117
+
1118
+ <script>
1119
+ // Set active filter tab based on URL
1120
+ function setActiveTab() {
1121
+ const urlParams = new URLSearchParams(window.location.search)
1122
+ const status = urlParams.get('status') || 'all'
1123
+ document.querySelectorAll('.filter-tab').forEach(tab => {
1124
+ const href = tab.getAttribute('href')
1125
+ const tabStatus = href === '/cron-requests' ? 'all' : href.split('=')[1]
1126
+ if (tabStatus === status) {
1127
+ tab.classList.add('active')
1128
+ } else {
1129
+ tab.classList.remove('active')
1130
+ }
1131
+ })
1132
+ }
1133
+
1134
+ // Render markdown in request messages
1135
+ function renderMarkdown() {
1136
+ if (typeof marked === 'undefined') return
1137
+ document.querySelectorAll('.message-content').forEach(el => {
1138
+ if (!el.hasAttribute('data-rendered')) {
1139
+ el.innerHTML = marked.parse(el.textContent || '')
1140
+ el.setAttribute('data-rendered', 'true')
1141
+ }
1142
+ })
1143
+ }
1144
+
1145
+ // Show status messages
1146
+ function showStatus(message, type = 'success') {
1147
+ const el = document.getElementById('action-status')
1148
+ if (!el) return
1149
+ el.textContent = message
1150
+ el.className = type
1151
+ el.classList.add('visible')
1152
+ clearTimeout(el._hideTimer)
1153
+ el._hideTimer = setTimeout(() => el.classList.remove('visible'), 4000)
1154
+ }
1155
+
1156
+ // Initial setup
1157
+ setActiveTab()
1158
+ renderMarkdown()
1159
+
1160
+ // HTMX event handlers
1161
+ document.addEventListener('htmx:afterRequest', (e) => {
1162
+ const xhr = e.detail.xhr
1163
+ if (xhr.status >= 200 && xhr.status < 300) {
1164
+ // Success - refresh the page
1165
+ setTimeout(() => window.location.reload(), 1000)
1166
+ showStatus('Action completed successfully')
1167
+ } else {
1168
+ // Error
1169
+ try {
1170
+ const response = JSON.parse(xhr.responseText)
1171
+ showStatus('Error: ' + (response.error || 'Unknown error'), 'error')
1172
+ } catch {
1173
+ showStatus('Error: Request failed', 'error')
1174
+ }
1175
+ }
1176
+ })
1177
+ </script>
1178
+ </body>
1179
+ </html>`;
1180
+ }
650
1181
  // ── Express app ───────────────────────────────────────────────────────────────
651
1182
  export async function appCoworkerChatWeb(options) {
652
1183
  const { url: agentUrl, password, host, port: portStr } = options;
@@ -712,6 +1243,57 @@ export async function appCoworkerChatWeb(options) {
712
1243
  res.send(renderDropdown(coworker ?? null, [{ name: humanName, status: null, isHuman: true, unreadMessages: 0 }]));
713
1244
  }
714
1245
  });
1246
+ // ── GET /cron-requests — cron requests page ──────────────────────────────
1247
+ app.get("/cron-requests", async (req, res) => {
1248
+ try {
1249
+ const status = req.query.status;
1250
+ const requests = await fetchCronRequests(agentUrl, password, status);
1251
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1252
+ res.send(renderCronRequestsPage(requests));
1253
+ }
1254
+ catch (err) {
1255
+ const msg = err instanceof Error ? err.message : String(err);
1256
+ res.status(502).send(`<pre>Error connecting to agent-office: ${escapeHtml(msg)}</pre>`);
1257
+ }
1258
+ });
1259
+ // ── POST /approve-request — approve a cron request ─────────────────────────
1260
+ app.post("/approve-request", async (req, res) => {
1261
+ const id = req.query.id;
1262
+ if (!id) {
1263
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1264
+ res.send(`<span style="color:var(--red)">Error: Request ID required</span>`);
1265
+ return;
1266
+ }
1267
+ try {
1268
+ await approveCronRequest(agentUrl, password, parseInt(id, 10));
1269
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1270
+ res.send(`<span style="color:var(--green)">✓ Request approved successfully</span>`);
1271
+ }
1272
+ catch (err) {
1273
+ const msg = err instanceof Error ? err.message : String(err);
1274
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1275
+ res.send(`<span style="color:var(--red)">✗ Approval failed: ${escapeHtml(msg)}</span>`);
1276
+ }
1277
+ });
1278
+ // ── POST /reject-request — reject a cron request ───────────────────────────
1279
+ app.post("/reject-request", async (req, res) => {
1280
+ const id = req.query.id;
1281
+ if (!id) {
1282
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1283
+ res.send(`<span style="color:var(--red)">Error: Request ID required</span>`);
1284
+ return;
1285
+ }
1286
+ try {
1287
+ await rejectCronRequest(agentUrl, password, parseInt(id, 10));
1288
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1289
+ res.send(`<span style="color:var(--green)">✓ Request rejected successfully</span>`);
1290
+ }
1291
+ catch (err) {
1292
+ const msg = err instanceof Error ? err.message : String(err);
1293
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1294
+ res.send(`<span style="color:var(--red)">✗ Rejection failed: ${escapeHtml(msg)}</span>`);
1295
+ }
1296
+ });
715
1297
  // ── GET / — full page ────────────────────────────────────────────────────
716
1298
  app.get("/", async (req, res) => {
717
1299
  try {
@@ -5,6 +5,9 @@ interface ServeOptions {
5
5
  port: string;
6
6
  password?: string;
7
7
  opencodeUrl: string;
8
+ piVendor?: string;
9
+ piModel?: string;
10
+ piApiKey?: string;
8
11
  }
9
12
  export declare function serve(options: ServeOptions): Promise<void>;
10
13
  export {};
@@ -1,6 +1,7 @@
1
1
  import { createPostgresqlStorage, createSqliteStorage } from "../db/index.js";
2
2
  import { runMigrations } from "../db/migrate.js";
3
3
  import { OpenCodeCodingServer } from "../lib/opencode-coding-server.js";
4
+ import { PiCodingServer } from "../lib/pi-coding-server.js";
4
5
  import { createApp } from "../server/index.js";
5
6
  import { CronScheduler } from "../server/cron.js";
6
7
  export async function serve(options) {
@@ -39,9 +40,35 @@ export async function serve(options) {
39
40
  await storage.close();
40
41
  process.exit(1);
41
42
  }
42
- // Init agentic coding server (OpenCode implementation)
43
- const agenticCodingServer = new OpenCodeCodingServer(options.opencodeUrl);
44
- console.log(`Connecting to OpenCode server at ${options.opencodeUrl}...`);
43
+ // Init agentic coding server
44
+ let agenticCodingServer;
45
+ const piVendor = options.piVendor;
46
+ const piModel = options.piModel;
47
+ const piApiKey = options.piApiKey;
48
+ const hasAllPiOptions = piVendor && piModel && piApiKey;
49
+ const hasSomePiOptions = piVendor || piModel || piApiKey;
50
+ if (hasAllPiOptions) {
51
+ // Use PI Coding Server
52
+ agenticCodingServer = new PiCodingServer(piVendor, piModel, piApiKey);
53
+ console.log(`Using PI coding server with ${piVendor} ${piModel}...`);
54
+ }
55
+ else if (hasSomePiOptions) {
56
+ // Partial PI options - error
57
+ console.error("Error: All PI coding options must be provided together: --pi-vendor, --pi-model, --pi-api-key");
58
+ console.error(" Or set environment variables: PI_VENDOR, PI_MODEL, PI_API_KEY");
59
+ await storage.close();
60
+ process.exit(1);
61
+ }
62
+ else {
63
+ // Use OpenCode Coding Server (default)
64
+ agenticCodingServer = new OpenCodeCodingServer(options.opencodeUrl);
65
+ if (options.opencodeUrl === "http://127.0.0.1:4096") {
66
+ console.log(`Connecting to default OpenCode server at ${options.opencodeUrl}...`);
67
+ }
68
+ else {
69
+ console.log(`Connecting to OpenCode server at ${options.opencodeUrl}...`);
70
+ }
71
+ }
45
72
  const serverUrl = `http://${options.host}:${port}`;
46
73
  // Create cron scheduler
47
74
  const cronScheduler = new CronScheduler();