ticlawk 0.1.16-dev.6 → 0.1.16-dev.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticlawk",
3
- "version": "0.1.16-dev.6",
3
+ "version": "0.1.16-dev.7",
4
4
  "description": "Local connector that links agent harnesses (Claude Code, Codex, OpenClaw, opencode, Pi) to the Ticlawk mobile app.",
5
5
  "type": "module",
6
6
  "main": "ticlawk.mjs",
@@ -436,6 +436,20 @@ export async function uploadAgentAttachment({
436
436
  });
437
437
  }
438
438
 
439
+ export async function uploadAgentAvatar({
440
+ actingAgentId, filename, contentType, dataBase64,
441
+ }) {
442
+ return apiFetch('/api/agent/profile/avatar', {
443
+ method: 'POST',
444
+ body: JSON.stringify({
445
+ acting_as_agent_id: actingAgentId,
446
+ filename,
447
+ content_type: contentType,
448
+ data_base64: dataBase64,
449
+ }),
450
+ });
451
+ }
452
+
439
453
  export async function viewAgentAttachment({ actingAgentId, assetId }) {
440
454
  const params = new URLSearchParams();
441
455
  params.set('acting_as_agent_id', actingAgentId);
@@ -545,13 +545,12 @@ export async function runProfileUpdateCommand(args) {
545
545
  }
546
546
  let avatarUrl = null;
547
547
  if (avatarFile) {
548
- // Re-use the attachment upload path then set the public_url as the avatar.
549
- const upload = await uploadFileViaDaemon(env, avatarFile);
548
+ const upload = await uploadAvatarViaDaemon(env, avatarFile);
550
549
  if (!upload.ok) {
551
550
  console.error(`avatar upload failed: ${upload.error}`);
552
551
  return 1;
553
552
  }
554
- avatarUrl = upload.publicUrl;
553
+ avatarUrl = upload.url;
555
554
  }
556
555
  const res = await daemonRequest({
557
556
  method: 'POST',
@@ -567,6 +566,34 @@ export async function runProfileUpdateCommand(args) {
567
566
  return exitFromStatus(res.statusCode);
568
567
  }
569
568
 
569
+ async function uploadAvatarViaDaemon(env, filePath) {
570
+ let stat;
571
+ try { stat = statSync(filePath); } catch (err) {
572
+ return { ok: false, error: `cannot stat ${filePath}: ${err.message}` };
573
+ }
574
+ if (!stat.isFile()) return { ok: false, error: `${filePath} is not a regular file` };
575
+ const contentType = inferContentType(filePath);
576
+ if (!contentType.startsWith('image/')) {
577
+ return { ok: false, error: `avatar must be an image (got content_type ${contentType})` };
578
+ }
579
+ const data = readFileSync(filePath);
580
+ const res = await daemonRequest({
581
+ method: 'POST',
582
+ path: '/agent/profile/avatar',
583
+ headers: commonHeaders(env),
584
+ body: {
585
+ filename: basename(filePath),
586
+ content_type: contentType,
587
+ data_base64: data.toString('base64'),
588
+ },
589
+ });
590
+ if (res.statusCode < 200 || res.statusCode >= 300) {
591
+ return { ok: false, error: res.body?.error || `HTTP ${res.statusCode}` };
592
+ }
593
+ if (!res.body?.url) return { ok: false, error: 'avatar upload returned no url' };
594
+ return { ok: true, url: res.body.url };
595
+ }
596
+
570
597
  async function uploadFileViaDaemon(env, filePath) {
571
598
  let stat;
572
599
  try { stat = statSync(filePath); } catch (err) {
@@ -588,7 +615,11 @@ async function uploadFileViaDaemon(env, filePath) {
588
615
  if (res.statusCode < 200 || res.statusCode >= 300) {
589
616
  return { ok: false, error: res.body?.error || `HTTP ${res.statusCode}` };
590
617
  }
591
- return { ok: true, assetId: res.body?.asset?.asset_id, publicUrl: res.body?.public_url };
618
+ const asset = res.body?.data;
619
+ if (!asset?.asset_id || !asset?.url) {
620
+ return { ok: false, error: 'attachment upload returned no asset' };
621
+ }
622
+ return { ok: true, assetId: asset.asset_id, url: asset.url, expiresAt: asset.expires_at };
592
623
  }
593
624
 
594
625
  function inferContentType(filePath) {
@@ -587,8 +587,8 @@ export async function handleAttachmentUpload(req, body, ctx) {
587
587
  });
588
588
  debugLog('agent-cli', 'attachment.upload', {
589
589
  actingAgentId,
590
- asset_id: data?.asset?.asset_id,
591
- bytes: data?.asset?.size_bytes,
590
+ asset_id: data?.data?.asset_id,
591
+ bytes: data?.data?.size_bytes,
592
592
  });
593
593
  return { status: 200, body: data };
594
594
  } catch (err) {
@@ -596,6 +596,28 @@ export async function handleAttachmentUpload(req, body, ctx) {
596
596
  }
597
597
  }
598
598
 
599
+ export async function handleProfileAvatarUpload(req, body, ctx) {
600
+ const actingAgentId = getActingAgentId(req, body);
601
+ const v = validateActingAgent(actingAgentId, ctx);
602
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
603
+ if (!body?.data_base64) return { status: 400, body: { error: 'data_base64 is required' } };
604
+ if (!body?.content_type || !String(body.content_type).startsWith('image/')) {
605
+ return { status: 400, body: { error: 'content_type must be image/*' } };
606
+ }
607
+ try {
608
+ const data = await api.uploadAgentAvatar({
609
+ actingAgentId,
610
+ filename: body?.filename || 'avatar.bin',
611
+ contentType: body.content_type,
612
+ dataBase64: body.data_base64,
613
+ });
614
+ debugLog('agent-cli', 'avatar.upload', { actingAgentId, url: data?.url });
615
+ return { status: 200, body: data };
616
+ } catch (err) {
617
+ return { status: err?.status || 500, body: { error: err?.message || 'avatar upload failed' } };
618
+ }
619
+ }
620
+
599
621
  export async function handleAttachmentView(req, query, ctx) {
600
622
  const actingAgentId = getActingAgentId(req, query);
601
623
  const v = validateActingAgent(actingAgentId, ctx);
package/src/core/http.mjs CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  handleMessageRead,
12
12
  handleMessageSearch,
13
13
  handleMessageSend,
14
+ handleProfileAvatarUpload,
14
15
  handleProfileShow,
15
16
  handleProfileUpdate,
16
17
  handleReminderCancel,
@@ -148,6 +149,12 @@ export function startLocalHttpServer({ port, adapter, ctx }) {
148
149
  const r = await handleProfileUpdate(req, body, cliCtx);
149
150
  return writeJson(res, r.status, r.body);
150
151
  }
152
+ if (urlNoQuery === '/agent/profile/avatar' && method === 'POST') {
153
+ const body = await readJsonBody(req);
154
+ if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
155
+ const r = await handleProfileAvatarUpload(req, body, cliCtx);
156
+ return writeJson(res, r.status, r.body);
157
+ }
151
158
  if (urlNoQuery === '/agent/attachment/upload' && method === 'POST') {
152
159
  const body = await readJsonBody(req);
153
160
  if (body === null) return writeJson(res, 400, { error: 'invalid json body' });