@heyamiko/amiko-cli 0.9.0-beta.15 → 0.9.0-beta.16
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/dist/index.js +272 -50
- package/package.json +1 -1
- package/skills/SKILL.md +41 -5
package/dist/index.js
CHANGED
|
@@ -5,43 +5,25 @@ var __getProtoOf = Object.getPrototypeOf;
|
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
function __accessProp(key) {
|
|
9
|
-
return this[key];
|
|
10
|
-
}
|
|
11
|
-
var __toESMCache_node;
|
|
12
|
-
var __toESMCache_esm;
|
|
13
8
|
var __toESM = (mod, isNodeMode, target) => {
|
|
14
|
-
var canCache = mod != null && typeof mod === "object";
|
|
15
|
-
if (canCache) {
|
|
16
|
-
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
17
|
-
var cached = cache.get(mod);
|
|
18
|
-
if (cached)
|
|
19
|
-
return cached;
|
|
20
|
-
}
|
|
21
9
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
22
10
|
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
23
11
|
for (let key of __getOwnPropNames(mod))
|
|
24
12
|
if (!__hasOwnProp.call(to, key))
|
|
25
13
|
__defProp(to, key, {
|
|
26
|
-
get:
|
|
14
|
+
get: () => mod[key],
|
|
27
15
|
enumerable: true
|
|
28
16
|
});
|
|
29
|
-
if (canCache)
|
|
30
|
-
cache.set(mod, to);
|
|
31
17
|
return to;
|
|
32
18
|
};
|
|
33
19
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
34
|
-
var __returnValue = (v) => v;
|
|
35
|
-
function __exportSetter(name, newValue) {
|
|
36
|
-
this[name] = __returnValue.bind(null, newValue);
|
|
37
|
-
}
|
|
38
20
|
var __export = (target, all) => {
|
|
39
21
|
for (var name in all)
|
|
40
22
|
__defProp(target, name, {
|
|
41
23
|
get: all[name],
|
|
42
24
|
enumerable: true,
|
|
43
25
|
configurable: true,
|
|
44
|
-
set:
|
|
26
|
+
set: (newValue) => all[name] = () => newValue
|
|
45
27
|
});
|
|
46
28
|
};
|
|
47
29
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -3533,36 +3515,11 @@ var require_emoji_regex = __commonJS((exports, module) => {
|
|
|
3533
3515
|
};
|
|
3534
3516
|
});
|
|
3535
3517
|
|
|
3536
|
-
// node_modules/mppx/dist/
|
|
3537
|
-
var
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
from: () => from
|
|
3542
|
-
});
|
|
3543
|
-
function from(method) {
|
|
3544
|
-
return method;
|
|
3545
|
-
}
|
|
3546
|
-
function toClient(method, options) {
|
|
3547
|
-
const { context, createCredential } = options;
|
|
3548
|
-
return {
|
|
3549
|
-
...method,
|
|
3550
|
-
context,
|
|
3551
|
-
createCredential
|
|
3552
|
-
};
|
|
3553
|
-
}
|
|
3554
|
-
function toServer(method, options) {
|
|
3555
|
-
const { defaults, html, request, respond, transport, verify } = options;
|
|
3556
|
-
return {
|
|
3557
|
-
...method,
|
|
3558
|
-
defaults,
|
|
3559
|
-
html,
|
|
3560
|
-
request,
|
|
3561
|
-
respond,
|
|
3562
|
-
transport,
|
|
3563
|
-
verify
|
|
3564
|
-
};
|
|
3565
|
-
}
|
|
3518
|
+
// node_modules/mppx/dist/BodyDigest.js
|
|
3519
|
+
var init_BodyDigest = () => {};
|
|
3520
|
+
|
|
3521
|
+
// node_modules/mppx/dist/PaymentRequest.js
|
|
3522
|
+
var init_PaymentRequest = () => {};
|
|
3566
3523
|
|
|
3567
3524
|
// node_modules/mppx/node_modules/zod/v4/core/core.js
|
|
3568
3525
|
function $constructor(name, initializer, params) {
|
|
@@ -16647,8 +16604,89 @@ var init_zod = __esm(() => {
|
|
|
16647
16604
|
init_mini();
|
|
16648
16605
|
});
|
|
16649
16606
|
|
|
16607
|
+
// node_modules/mppx/dist/Challenge.js
|
|
16608
|
+
var Schema;
|
|
16609
|
+
var init_Challenge = __esm(() => {
|
|
16610
|
+
init_zod();
|
|
16611
|
+
Schema = object({
|
|
16612
|
+
description: optional(string2()),
|
|
16613
|
+
digest: optional(string2().check(_regex(/^sha-256=/, "Invalid digest format"))),
|
|
16614
|
+
expires: optional(datetime3()),
|
|
16615
|
+
id: string2(),
|
|
16616
|
+
intent: string2(),
|
|
16617
|
+
method: string2(),
|
|
16618
|
+
opaque: optional(record(string2(), string2())),
|
|
16619
|
+
realm: string2(),
|
|
16620
|
+
request: record(string2(), unknown())
|
|
16621
|
+
});
|
|
16622
|
+
});
|
|
16623
|
+
|
|
16624
|
+
// node_modules/mppx/dist/Credential.js
|
|
16625
|
+
var init_Credential = () => {};
|
|
16626
|
+
|
|
16627
|
+
// node_modules/mppx/dist/Errors.js
|
|
16628
|
+
var init_Errors = () => {};
|
|
16629
|
+
|
|
16630
|
+
// node_modules/mppx/dist/Expires.js
|
|
16631
|
+
var init_Expires = () => {};
|
|
16632
|
+
|
|
16633
|
+
// node_modules/mppx/dist/Method.js
|
|
16634
|
+
var exports_Method = {};
|
|
16635
|
+
__export(exports_Method, {
|
|
16636
|
+
toServer: () => toServer,
|
|
16637
|
+
toClient: () => toClient,
|
|
16638
|
+
from: () => from
|
|
16639
|
+
});
|
|
16640
|
+
function from(method) {
|
|
16641
|
+
return method;
|
|
16642
|
+
}
|
|
16643
|
+
function toClient(method, options) {
|
|
16644
|
+
const { context, createCredential } = options;
|
|
16645
|
+
return {
|
|
16646
|
+
...method,
|
|
16647
|
+
context,
|
|
16648
|
+
createCredential
|
|
16649
|
+
};
|
|
16650
|
+
}
|
|
16651
|
+
function toServer(method, options) {
|
|
16652
|
+
const { defaults, html, request, respond, transport, verify } = options;
|
|
16653
|
+
return {
|
|
16654
|
+
...method,
|
|
16655
|
+
defaults,
|
|
16656
|
+
html,
|
|
16657
|
+
request,
|
|
16658
|
+
respond,
|
|
16659
|
+
transport,
|
|
16660
|
+
verify
|
|
16661
|
+
};
|
|
16662
|
+
}
|
|
16663
|
+
|
|
16664
|
+
// node_modules/mppx/dist/Receipt.js
|
|
16665
|
+
var Schema2;
|
|
16666
|
+
var init_Receipt = __esm(() => {
|
|
16667
|
+
init_zod();
|
|
16668
|
+
Schema2 = object({
|
|
16669
|
+
method: string2(),
|
|
16670
|
+
reference: string2(),
|
|
16671
|
+
externalId: optional(string2()),
|
|
16672
|
+
status: literal("success"),
|
|
16673
|
+
timestamp: datetime3()
|
|
16674
|
+
});
|
|
16675
|
+
});
|
|
16676
|
+
|
|
16677
|
+
// node_modules/mppx/dist/Store.js
|
|
16678
|
+
var init_Store = () => {};
|
|
16679
|
+
|
|
16650
16680
|
// node_modules/mppx/dist/index.js
|
|
16651
16681
|
var init_dist = __esm(() => {
|
|
16682
|
+
init_BodyDigest();
|
|
16683
|
+
init_Challenge();
|
|
16684
|
+
init_Credential();
|
|
16685
|
+
init_Errors();
|
|
16686
|
+
init_Expires();
|
|
16687
|
+
init_PaymentRequest();
|
|
16688
|
+
init_Receipt();
|
|
16689
|
+
init_Store();
|
|
16652
16690
|
init_zod();
|
|
16653
16691
|
});
|
|
16654
16692
|
|
|
@@ -21138,6 +21176,190 @@ Accept with: amiko friends accept <friendshipId>`));
|
|
|
21138
21176
|
process.exit(1);
|
|
21139
21177
|
}
|
|
21140
21178
|
});
|
|
21179
|
+
reports.command("pending").description("List pending-consent report requests (awaiting my consent + awaiting theirs)").option("--limit <n>", "Page size (1-50)", "50").option("--json", "Output as JSON").action(async (opts) => {
|
|
21180
|
+
const auth = resolveAuth();
|
|
21181
|
+
const me = auth.userId;
|
|
21182
|
+
if (!me && !opts.json) {
|
|
21183
|
+
console.error(error("Current user id unknown — cannot split requests. Run `amiko accounts` to check."));
|
|
21184
|
+
process.exit(1);
|
|
21185
|
+
}
|
|
21186
|
+
const spinner = opts.json ? null : ora("Loading...").start();
|
|
21187
|
+
try {
|
|
21188
|
+
const data = await amikoWebFetch(auth, "/api/friends/reports", {
|
|
21189
|
+
query: { page: "1", limit: opts.limit }
|
|
21190
|
+
});
|
|
21191
|
+
spinner?.stop();
|
|
21192
|
+
const items = (data.items ?? []).filter((r) => r.status === "pending_consent");
|
|
21193
|
+
const incoming = items.filter((r) => r.respondent_id === me);
|
|
21194
|
+
const outgoing = items.filter((r) => r.initiator_id === me);
|
|
21195
|
+
renderOutput({ incoming, outgoing }, () => {
|
|
21196
|
+
console.log(heading(`Awaiting my consent (${incoming.length})`));
|
|
21197
|
+
if (incoming.length === 0) {
|
|
21198
|
+
console.log(dim(" none"));
|
|
21199
|
+
} else {
|
|
21200
|
+
const rows = [
|
|
21201
|
+
[
|
|
21202
|
+
dim("REPORT ID"),
|
|
21203
|
+
dim("TYPE"),
|
|
21204
|
+
dim("FROM"),
|
|
21205
|
+
dim("HANDLE"),
|
|
21206
|
+
dim("SENT")
|
|
21207
|
+
],
|
|
21208
|
+
...incoming.map((r) => [
|
|
21209
|
+
r.id,
|
|
21210
|
+
r.report_type ?? "-",
|
|
21211
|
+
r.initiator?.name ?? "-",
|
|
21212
|
+
r.initiator?.handle ?? "-",
|
|
21213
|
+
r.created_at?.slice(0, 10) ?? "-"
|
|
21214
|
+
])
|
|
21215
|
+
];
|
|
21216
|
+
console.log(table(rows));
|
|
21217
|
+
console.log(dim(`
|
|
21218
|
+
Consent with: amiko friends reports consent <reportId>`));
|
|
21219
|
+
}
|
|
21220
|
+
console.log();
|
|
21221
|
+
console.log(heading(`Awaiting their consent (${outgoing.length})`));
|
|
21222
|
+
if (outgoing.length === 0) {
|
|
21223
|
+
console.log(dim(" none"));
|
|
21224
|
+
} else {
|
|
21225
|
+
const rows = [
|
|
21226
|
+
[
|
|
21227
|
+
dim("REPORT ID"),
|
|
21228
|
+
dim("TYPE"),
|
|
21229
|
+
dim("TO"),
|
|
21230
|
+
dim("HANDLE"),
|
|
21231
|
+
dim("SENT")
|
|
21232
|
+
],
|
|
21233
|
+
...outgoing.map((r) => [
|
|
21234
|
+
r.id,
|
|
21235
|
+
r.report_type ?? "-",
|
|
21236
|
+
r.respondent?.name ?? "-",
|
|
21237
|
+
r.respondent?.handle ?? "-",
|
|
21238
|
+
r.created_at?.slice(0, 10) ?? "-"
|
|
21239
|
+
])
|
|
21240
|
+
];
|
|
21241
|
+
console.log(table(rows));
|
|
21242
|
+
console.log(dim(`
|
|
21243
|
+
Cancel with: amiko friends reports cancel <reportId>`));
|
|
21244
|
+
}
|
|
21245
|
+
}, { json: opts.json });
|
|
21246
|
+
} catch (e) {
|
|
21247
|
+
spinner?.stop();
|
|
21248
|
+
console.error(error(e instanceof Error ? e.message : String(e)));
|
|
21249
|
+
process.exit(1);
|
|
21250
|
+
}
|
|
21251
|
+
});
|
|
21252
|
+
reports.command("request").description("Request a friend matching report (notifies the other user to consent)").requiredOption("--id <userId>", "Target user id (must be an accepted friend)").requiredOption("--type <friend|romantic|career>", "Report type").option("--yes", "Skip confirmation").option("--json", "Output as JSON").action(async (opts) => {
|
|
21253
|
+
const allowed = ["friend", "romantic", "career"];
|
|
21254
|
+
if (!allowed.includes(opts.type)) {
|
|
21255
|
+
console.error(error(`--type must be one of: ${allowed.join(", ")}`));
|
|
21256
|
+
process.exit(1);
|
|
21257
|
+
}
|
|
21258
|
+
await confirmDestructive({
|
|
21259
|
+
action: `Request a ${opts.type} matching report with user ${opts.id} (they will be notified)`,
|
|
21260
|
+
yes: opts.yes,
|
|
21261
|
+
commandExample: `amiko friends reports request --id ${opts.id} --type ${opts.type}`
|
|
21262
|
+
});
|
|
21263
|
+
const auth = resolveAuth();
|
|
21264
|
+
const spinner = opts.json ? null : ora("Requesting report...").start();
|
|
21265
|
+
try {
|
|
21266
|
+
const data = await amikoWebFetch(auth, "/api/friends/reports", {
|
|
21267
|
+
method: "POST",
|
|
21268
|
+
body: { friend_id: opts.id, report_type: opts.type }
|
|
21269
|
+
});
|
|
21270
|
+
spinner?.stop();
|
|
21271
|
+
renderOutput(data, (d) => {
|
|
21272
|
+
if (d.report) {
|
|
21273
|
+
console.log(success(`Report requested (${d.report.report_type})`));
|
|
21274
|
+
console.log(label("Report ID", d.report.id));
|
|
21275
|
+
console.log(label("Status", d.report.status));
|
|
21276
|
+
console.log(dim(`
|
|
21277
|
+
The other user will be notified and must consent before the report is generated.`));
|
|
21278
|
+
}
|
|
21279
|
+
}, { json: opts.json });
|
|
21280
|
+
} catch (e) {
|
|
21281
|
+
spinner?.stop();
|
|
21282
|
+
console.error(error(e instanceof Error ? e.message : String(e)));
|
|
21283
|
+
process.exit(1);
|
|
21284
|
+
}
|
|
21285
|
+
});
|
|
21286
|
+
for (const action of ["consent", "cancel", "retry"]) {
|
|
21287
|
+
const desc = {
|
|
21288
|
+
consent: "Consent to a report request (respondent only)",
|
|
21289
|
+
cancel: "Cancel a pending report",
|
|
21290
|
+
retry: "Retry a failed report generation"
|
|
21291
|
+
};
|
|
21292
|
+
reports.command(`${action} <id>`).description(desc[action]).option("--json", "Output as JSON").action(async (id, opts) => {
|
|
21293
|
+
const auth = resolveAuth();
|
|
21294
|
+
const spinner = opts.json ? null : ora(`${action}...`).start();
|
|
21295
|
+
try {
|
|
21296
|
+
const data = await amikoWebFetch(auth, `/api/friends/reports/${encodeURIComponent(id)}`, { method: "PATCH", body: { action } });
|
|
21297
|
+
spinner?.stop();
|
|
21298
|
+
renderOutput(data, (d) => {
|
|
21299
|
+
console.log(success(`${action} ok`));
|
|
21300
|
+
if (d.status)
|
|
21301
|
+
console.log(label("Status", d.status));
|
|
21302
|
+
}, { json: opts.json });
|
|
21303
|
+
} catch (e) {
|
|
21304
|
+
spinner?.stop();
|
|
21305
|
+
console.error(error(e instanceof Error ? e.message : String(e)));
|
|
21306
|
+
process.exit(1);
|
|
21307
|
+
}
|
|
21308
|
+
});
|
|
21309
|
+
}
|
|
21310
|
+
program2.command("find").description("Find matches for a custom relationship description (LLM-generated, on-demand)").requiredOption("--relationship <text>", 'Relationship description, e.g. "cofounder who is good at design"').option("--rationale <text>", "Optional extra context for the matcher").option("--limit <n>", "Max results (1-20)", "10").option("--json", "Output as JSON").action(async (opts) => {
|
|
21311
|
+
const limitNum = Math.max(1, Math.min(20, Number(opts.limit) || 10));
|
|
21312
|
+
const auth = resolveAuth();
|
|
21313
|
+
const spinner = opts.json ? null : ora("Finding matches (may take ~10s)...").start();
|
|
21314
|
+
try {
|
|
21315
|
+
const data = await amikoWebFetch(auth, "/api/user/personality-profile/matches", {
|
|
21316
|
+
method: "POST",
|
|
21317
|
+
body: {
|
|
21318
|
+
relationship_type: opts.relationship,
|
|
21319
|
+
rationale: opts.rationale,
|
|
21320
|
+
limit: limitNum
|
|
21321
|
+
}
|
|
21322
|
+
});
|
|
21323
|
+
spinner?.stop();
|
|
21324
|
+
renderOutput(data, (d) => {
|
|
21325
|
+
const matches = d.matches ?? [];
|
|
21326
|
+
if (matches.length === 0) {
|
|
21327
|
+
console.log(dim("No matches."));
|
|
21328
|
+
if (d.no_match_reason)
|
|
21329
|
+
console.log(dim(d.no_match_reason));
|
|
21330
|
+
return;
|
|
21331
|
+
}
|
|
21332
|
+
console.log(heading(`Matches for "${opts.relationship}" (${matches.length})`));
|
|
21333
|
+
const rows = [
|
|
21334
|
+
[
|
|
21335
|
+
dim("USER"),
|
|
21336
|
+
dim("NAME"),
|
|
21337
|
+
dim("SCORE"),
|
|
21338
|
+
dim("STATUS"),
|
|
21339
|
+
dim("PAIRING")
|
|
21340
|
+
],
|
|
21341
|
+
...matches.map((m) => [
|
|
21342
|
+
m.user_id,
|
|
21343
|
+
m.display_name ?? "-",
|
|
21344
|
+
m.score.toFixed(3),
|
|
21345
|
+
m.friendship_status ?? "none",
|
|
21346
|
+
m.pairing_label ?? "-"
|
|
21347
|
+
])
|
|
21348
|
+
];
|
|
21349
|
+
console.log(table(rows));
|
|
21350
|
+
for (const m of matches) {
|
|
21351
|
+
if (m.compatibility_highlights?.length) {
|
|
21352
|
+
console.log();
|
|
21353
|
+
console.log(label(m.display_name ?? m.user_id, m.compatibility_highlights.join("; ")));
|
|
21354
|
+
}
|
|
21355
|
+
}
|
|
21356
|
+
}, { json: opts.json });
|
|
21357
|
+
} catch (e) {
|
|
21358
|
+
spinner?.stop();
|
|
21359
|
+
console.error(error(e instanceof Error ? e.message : String(e)));
|
|
21360
|
+
process.exit(1);
|
|
21361
|
+
}
|
|
21362
|
+
});
|
|
21141
21363
|
program2.command("matches").description("List friend matches").option("--limit <n>", "Max results (1-50)", "10").option("--offset <n>", "Offset", "0").option("--relationship-type <type>", "Filter by relationship type").option("--dimension <dimension>", "Filter by match dimension").option("--json", "Output as JSON").action(async (opts) => {
|
|
21142
21364
|
const auth = resolveAuth();
|
|
21143
21365
|
const spinner = opts.json ? null : ora("Loading matches...").start();
|
package/package.json
CHANGED
package/skills/SKILL.md
CHANGED
|
@@ -51,7 +51,7 @@ Every paid command (`markets search`, `markets image`, `markets amazon search`,
|
|
|
51
51
|
|
|
52
52
|
## Critical Rules
|
|
53
53
|
|
|
54
|
-
1. **Every paid / value-moving command is hard-gated on `--yes` in a non-interactive shell.** The CLI *refuses* to run `markets image`, `markets search`, `markets amazon search`, `markets service call`, `markets service tts`, `markets service stt`, `markets service chat`, `credits topup`, `wallets swap send`, or `wallets bridge send` unless `--yes` is passed. The refusal is loud: it prints the cost and the command you should re-run. **Quote the cost to the user, get explicit approval, THEN append `--yes`**. Same for destructive non-paid commands (`twin update --public`, `docs delete`, `friends remove`, `avatar update`, `voice reset`, `composio disconnect`).
|
|
54
|
+
1. **Every paid / value-moving command is hard-gated on `--yes` in a non-interactive shell.** The CLI *refuses* to run `markets image`, `markets search`, `markets amazon search`, `markets service call`, `markets service tts`, `markets service stt`, `markets service chat`, `credits topup`, `wallets swap send`, or `wallets bridge send` unless `--yes` is passed. The refusal is loud: it prints the cost and the command you should re-run. **Quote the cost to the user, get explicit approval, THEN append `--yes`**. Same for destructive non-paid commands (`twin update --public`, `docs delete`, `friends remove`, `friends reports request`, `avatar update`, `voice reset`, `composio disconnect`).
|
|
55
55
|
2. **After every paid command, report the remaining balance.** The CLI prints a `Balance: N credits` line at the end of every paid command — include that figure in your reply.
|
|
56
56
|
3. **Never retry a failed command.** Report the error and stop. Every paid call costs tokens even on failure.
|
|
57
57
|
4. **Never suggest `amiko login` or `amiko connect`.** These don't exist. Auth is automatic when you run from your workspace folder.
|
|
@@ -188,12 +188,48 @@ amiko friends requests --direction incoming
|
|
|
188
188
|
amiko friends add --id <userId>
|
|
189
189
|
amiko friends accept <friendshipId>
|
|
190
190
|
amiko friends remove <friendshipId> --yes # destructive
|
|
191
|
-
amiko friends matches #
|
|
192
|
-
amiko friends matches --
|
|
193
|
-
amiko friends
|
|
194
|
-
amiko friends reports
|
|
191
|
+
amiko friends matches # pre-generated match candidates (from cron)
|
|
192
|
+
amiko friends matches --dimension personality
|
|
193
|
+
amiko friends find --relationship "cofounder with design taste" # on-demand LLM match for a custom relationship
|
|
194
|
+
amiko friends reports list # all reports (any status)
|
|
195
|
+
amiko friends reports pending # pending-consent: awaiting mine + awaiting theirs
|
|
196
|
+
amiko friends reports view <reportId>
|
|
197
|
+
amiko friends reports request --id <userId> --type friend|romantic|career --yes
|
|
198
|
+
amiko friends reports consent <reportId> # respondent approves; report then generates
|
|
199
|
+
amiko friends reports cancel <reportId>
|
|
200
|
+
amiko friends reports retry <reportId> # re-run a failed generation
|
|
195
201
|
```
|
|
196
202
|
|
|
203
|
+
### Answering relationship questions ("how is my relationship with X?")
|
|
204
|
+
|
|
205
|
+
When the user asks about their relationship / compatibility with a specific person (by name or handle), follow this playbook:
|
|
206
|
+
|
|
207
|
+
1. **Resolve the person to a user id.** Check `amiko friends list --json` first. If not a friend, try `amiko users search "<name>" --json`. If still ambiguous, ask the user which one they mean.
|
|
208
|
+
2. **Look for an existing report.** Run `amiko friends reports list --json` and filter by `initiator.id` or `respondent.id` matching the resolved user id. If one exists with `status=completed`, use `amiko friends reports view <id>` and summarize. If `status=pending_consent` or `generating`, tell the user it is in progress.
|
|
209
|
+
3. **If none exists**, don't silently create one. Ask the user which of the three report types they want (`friend` / `romantic` / `career`), and confirm that the other user will be notified to consent. Only then run `amiko friends reports request --id <userId> --type <type> --yes`.
|
|
210
|
+
4. **For "who is waiting on whom"**, use `amiko friends reports pending` — it splits `pending_consent` reports into *awaiting my consent* (I can `consent <id>`) vs *awaiting theirs* (I can `cancel <id>`). Use this before deciding to `request` — you cannot have two active reports of the same type between the same pair (the API returns 409).
|
|
211
|
+
|
|
212
|
+
Both users must have a personality profile, otherwise the request returns 422.
|
|
213
|
+
|
|
214
|
+
### Recommending interesting people ("who should I meet / connect with?")
|
|
215
|
+
|
|
216
|
+
The discovery primitive is **`amiko friends matches`** — it returns personality-match candidates with a score (0–1), a match dimension, a pairing label, and compatibility highlights.
|
|
217
|
+
|
|
218
|
+
**`friends find` vs `users search` — do not confuse them:**
|
|
219
|
+
|
|
220
|
+
| Command | Purpose | Input | Backend |
|
|
221
|
+
|---|---|---|---|
|
|
222
|
+
| `users search <query>` | **Look up a specific known person** by name or handle ("find wendao", "search for someone called Sophie") | Exact/substring text against `name` / `handle` | Simple SQL lookup in `/api/search?type=people` |
|
|
223
|
+
| `friends find --relationship <text>` | **Discover unknown people** whose personality matches a free-form relationship description ("cofounder with design taste", "someone to hike with") | Free-form relationship description | LLM-generated MatchingSpec + semantic matching across ~250 candidate profiles |
|
|
224
|
+
|
|
225
|
+
Rule of thumb: if the user already has a name in mind → `users search`. If the user is describing what *kind of person* they want to meet → `friends find`. For already-curated ambient recommendations with no input required → `friends matches`.
|
|
226
|
+
|
|
227
|
+
1. Start broad: `amiko friends matches --limit 20 --json`. Summarize the top few by `display_name`, `score`, `match_dimension`, `pairing_label`, and `recommendation_reason`. These are pre-generated by a cron job.
|
|
228
|
+
2. If the user wants to narrow, filter by `--dimension personality` or `--dimension interest` (the only two dimensions the matching cron actually produces). `--relationship-type` also exists but takes free-form LLM-generated labels (often localized, e.g. `深度思维_对打手`) — usually not worth filtering on unless you first inspect the `--json` output to pick a known value.
|
|
229
|
+
3. If the user describes a **specific kind of person** not covered by the cron matches ("a cofounder who is good at design", "a hiking buddy", "someone to debate philosophy with"), use `amiko friends find --relationship "<description>"`. This is an on-demand, LLM-backed matcher against ~250 candidates; may take ~10s and may return zero matches if nothing clears the internal score threshold.
|
|
230
|
+
4. Skip anyone whose `friendship_status` is already `accepted` unless the user explicitly wants to revisit existing friends.
|
|
231
|
+
5. Follow-ups: `amiko users profile <handle>` for a fuller view, `amiko friends add --id <matched_user_id>` (after explicit user approval) to send a friend request, and — once the friendship is accepted — `amiko friends reports request --type friend|romantic|career` for a deeper compatibility report. Note that report `--type` is a separate enum and has nothing to do with `match.relationship_type`.
|
|
232
|
+
|
|
197
233
|
## Users
|
|
198
234
|
|
|
199
235
|
```bash
|